mirror of
https://github.com/pretix/pretix.git
synced 2026-01-25 00:42:26 +00:00
Compare commits
3 Commits
v4.10.1
...
release/4.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f91d1a6dc | ||
|
|
1a091c9705 | ||
|
|
115e1af96b |
2
doc/_themes/pretix_theme/layout.html
vendored
2
doc/_themes/pretix_theme/layout.html
vendored
@@ -157,7 +157,7 @@
|
||||
<div class="rst-content">
|
||||
{% include "breadcrumbs.html" %}
|
||||
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||||
<div itemprop="articleBody" class="section">
|
||||
<div itemprop="articleBody">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
<div class="articleComments">
|
||||
|
||||
@@ -172,6 +172,8 @@ Cart position endpoints
|
||||
|
||||
* does not check or calculate prices but believes any prices you send
|
||||
|
||||
* does not support the redemption of vouchers
|
||||
|
||||
* does not prevent you from buying items that can only be bought with a voucher
|
||||
|
||||
* does not support file upload questions
|
||||
@@ -187,9 +189,8 @@ Cart position endpoints
|
||||
* ``attendee_email`` (optional)
|
||||
* ``subevent`` (optional)
|
||||
* ``expires`` (optional)
|
||||
* ``includes_tax`` (optional, **deprecated**, do not use, will be removed)
|
||||
* ``includes_tax`` (optional)
|
||||
* ``sales_channel`` (optional)
|
||||
* ``voucher`` (optional, expect a voucher code)
|
||||
* ``answers``
|
||||
|
||||
* ``question``
|
||||
|
||||
@@ -14,7 +14,6 @@ The customer resource contains the following public fields:
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
identifier string Internal ID of the customer
|
||||
external_identifier string External ID of the customer (or ``null``)
|
||||
email string Customer email address
|
||||
name string Name of this customer (or ``null``)
|
||||
name_parts object of strings Decomposition of name (i.e. given name, family name)
|
||||
@@ -25,7 +24,6 @@ last_login datetime Date and time o
|
||||
date_joined datetime Date and time of registration
|
||||
locale string Preferred language of the customer
|
||||
last_modified datetime Date and time of modification of the record
|
||||
notes string Internal notes and comments (or ``null``)
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionadded:: 4.0
|
||||
@@ -60,7 +58,6 @@ Endpoints
|
||||
"results": [
|
||||
{
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "customer@example.org",
|
||||
"name": "John Doe",
|
||||
"name_parts": {
|
||||
@@ -72,8 +69,7 @@ Endpoints
|
||||
"last_login": null,
|
||||
"date_joined": "2021-04-06T13:44:22.809216Z",
|
||||
"locale": "de",
|
||||
"last_modified": "2021-04-06T13:44:22.809377Z",
|
||||
"notes": null
|
||||
"last_modified": "2021-04-06T13:44:22.809377Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -107,7 +103,6 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "customer@example.org",
|
||||
"name": "John Doe",
|
||||
"name_parts": {
|
||||
@@ -119,8 +114,7 @@ Endpoints
|
||||
"last_login": null,
|
||||
"date_joined": "2021-04-06T13:44:22.809216Z",
|
||||
"locale": "de",
|
||||
"last_modified": "2021-04-06T13:44:22.809377Z",
|
||||
"notes": null
|
||||
"last_modified": "2021-04-06T13:44:22.809377Z"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -156,7 +150,6 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "test@example.org",
|
||||
...
|
||||
}
|
||||
@@ -200,7 +193,6 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "test@example.org",
|
||||
…
|
||||
}
|
||||
@@ -234,7 +226,6 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": null,
|
||||
…
|
||||
}
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
.. _`rest-discounts`:
|
||||
|
||||
Discounts
|
||||
=========
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
Discounts provide a way to automatically reduce the price of a cart if it matches a given set of conditions.
|
||||
Discounts are available to everyone. If you want to give a discount just to specific persons, look at
|
||||
:ref:`vouchers <rest-vouchers>` instead. If you are interested in the behind-the-scenes details of how
|
||||
discounts are calculated for a specific order, have a look at :ref:`our algorithm documentation <algorithms-pricing>`.
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
======================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
======================================== ========================== =======================================================
|
||||
id integer Internal ID of the discount rule
|
||||
active boolean The discount will be ignored if this is ``false``
|
||||
internal_name string A name for the rule used in the backend
|
||||
position integer An integer, used for sorting the rules which are applied in order
|
||||
sales_channels list of strings Sales channels this discount is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
available_from datetime The first date time at which this discount can be applied
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this discount can be applied
|
||||
(or ``null``).
|
||||
subevent_mode strings Determines how the discount is handled when used in an
|
||||
event series. Can be ``"mixed"`` (no special effect),
|
||||
``"same"`` (discount is only applied for groups within
|
||||
the same date), or ``"distinct"`` (discount is only applied
|
||||
for groups with no two same dates).
|
||||
condition_all_products boolean If ``true``, the discount applies to all items.
|
||||
condition_limit_products list of integers If ``condition_all_products`` is not set, this is a list
|
||||
of internal item IDs that the discount applies to.
|
||||
condition_apply_to_addons boolean If ``true``, the discount applies to add-on products as well,
|
||||
otherwise it only applies to top-level items. The discount never
|
||||
applies to bundled products.
|
||||
condition_ignore_voucher_discounted boolean If ``true``, the discount does not apply to products which have
|
||||
been discounted by a voucher.
|
||||
condition_min_count integer The minimum number of matching products for the discount
|
||||
to be activated.
|
||||
condition_min_value money (string) The minimum value of matching products for the discount
|
||||
to be activated. Cannot be combined with ``condition_min_count``,
|
||||
or with ``subevent_mode`` set to ``distinct``.
|
||||
benefit_discount_matching_percent decimal (string) The percentage of price reduction for matching products.
|
||||
benefit_only_apply_to_cheapest_n_matches integer If set higher than 0, the discount will only be applied to
|
||||
the cheapest matches. Useful for a "3 for 2"-style discount.
|
||||
Cannot be combined with ``condition_min_value``.
|
||||
======================================== ========================== =======================================================
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/discounts/
|
||||
|
||||
Returns a list of all discounts within a given event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/discounts/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
"condition_ignore_voucher_discounted": false,
|
||||
"condition_min_count": 3,
|
||||
"condition_min_value": "0.00",
|
||||
"benefit_discount_matching_percent": "100.00",
|
||||
"benefit_only_apply_to_cheapest_n_matches": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query boolean active: If set to ``true`` or ``false``, only discounts with this value for the field ``active`` will be
|
||||
returned.
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
|
||||
Default: ``position``
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/discounts/(id)/
|
||||
|
||||
Returns information on one discount, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/discounts/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
"condition_ignore_voucher_discounted": false,
|
||||
"condition_min_count": 3,
|
||||
"condition_min_value": "0.00",
|
||||
"benefit_discount_matching_percent": "100.00",
|
||||
"benefit_only_apply_to_cheapest_n_matches": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``id`` field of the discount to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/discounts/
|
||||
|
||||
Creates a new discount
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/discounts/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
"condition_ignore_voucher_discounted": false,
|
||||
"condition_min_count": 3,
|
||||
"condition_min_value": "0.00",
|
||||
"benefit_discount_matching_percent": "100.00",
|
||||
"benefit_only_apply_to_cheapest_n_matches": 1
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
"condition_ignore_voucher_discounted": false,
|
||||
"condition_min_count": 3,
|
||||
"condition_min_value": "0.00",
|
||||
"benefit_discount_matching_percent": "100.00",
|
||||
"benefit_only_apply_to_cheapest_n_matches": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to create a discount for
|
||||
:param event: The ``slug`` field of the event to create a discount for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The discount could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/discounts/(id)/
|
||||
|
||||
Update a discount. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``id`` field.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/discounts/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"active": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"active": false,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
"subevent_mode": "mixed",
|
||||
"condition_all_products": true,
|
||||
"condition_limit_products": [],
|
||||
"condition_apply_to_addons": true,
|
||||
"condition_ignore_voucher_discounted": false,
|
||||
"condition_min_count": 3,
|
||||
"condition_min_value": "0.00",
|
||||
"benefit_discount_matching_percent": "100.00",
|
||||
"benefit_only_apply_to_cheapest_n_matches": 1
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the discount to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The discount could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/discount/(id)/
|
||||
|
||||
Delete a discount.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/discount/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the discount to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||
@@ -24,7 +24,6 @@ at :ref:`plugin-docs`.
|
||||
orders
|
||||
invoices
|
||||
vouchers
|
||||
discounts
|
||||
checkinlists
|
||||
waitinglist
|
||||
customers
|
||||
|
||||
@@ -609,17 +609,13 @@ 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
|
||||
be a ZIP file, PDF file or something else. The order details response contains a list of output options for this
|
||||
particular order.
|
||||
|
||||
Tickets can only be downloaded if ticket downloads are active and – depending on event settings – the order is either paid or pending. Note that in some cases the
|
||||
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Note that in some cases the
|
||||
ticket file might not yet have been created. In that case, you will receive a status code :http:statuscode:`409` and
|
||||
you are expected to retry the request after a short period of waiting.
|
||||
|
||||
@@ -1639,10 +1635,6 @@ 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.
|
||||
@@ -1654,7 +1646,7 @@ Order position ticket download
|
||||
The referenced URL can provide a download or a regular, human-viewable website - so it is advised to open this URL
|
||||
in a webbrowser and leave it up to the user to handle the result.
|
||||
|
||||
Tickets can only be downloaded if ticket downloads are active and – depending on event settings – the order is either paid or pending. Also, depending on event
|
||||
Tickets can be only downloaded if the order is paid and if ticket downloads are active. Also, depending on event
|
||||
configuration downloads might be only unavailable for add-on products or non-admission products.
|
||||
Note that in some cases the ticket file might not yet have been created. In that case, you will receive a status
|
||||
code :http:statuscode:`409` and you are expected to retry the request after a short period of waiting.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
.. _`rest-vouchers`:
|
||||
|
||||
Vouchers
|
||||
========
|
||||
|
||||
|
||||
@@ -9,6 +9,5 @@ ticket scanning apps and we want to ensure the implementations are as similar as
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
pricing
|
||||
checkin
|
||||
layouts
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
.. _`algorithms-pricing`:
|
||||
|
||||
Pricing algorithms
|
||||
==================
|
||||
|
||||
With pretix being an e-commerce application, one of its core tasks is to determine the price of a purchase. With the
|
||||
complexity allowed by our range of features, this is not a trivial task and there are many edge cases that need to be
|
||||
clearly defined. The most challenging part about this is that there are many situations in which a price might change
|
||||
while the user is going through the checkout process and we're learning more information about them or their purchase.
|
||||
For example, prices change when
|
||||
|
||||
* The cart expires and the listed prices changed in the meantime
|
||||
* The user adds an invoice address that triggers a change in taxation
|
||||
* The user chooses a custom price for an add-on product and adjusts the price later on
|
||||
* The user adds a voucher to their cart
|
||||
* An automatic discount is applied
|
||||
|
||||
For the purposes of this page, we're making a distinction between "naive prices" (which are just a plain number like 23.00), and
|
||||
"taxed prices" (which are a combination of a net price, a tax rate, and a gross price, like 19.33 + 19% = 23.00).
|
||||
|
||||
Computation of listed prices
|
||||
----------------------------
|
||||
|
||||
When showing a list of products, e.g. on the event front page, we always need to show a price. This price is what we
|
||||
call the "listed price" later on.
|
||||
|
||||
To compute the listed price, we first use the ``default_price`` attribute of the ``Item`` that is being shown.
|
||||
If we are showing an ``ItemVariation`` and that variation has a ``default_price`` set on itself, the variation's price
|
||||
takes precedence and replaces the item's price.
|
||||
If we're in an event series and there exists a ``SubEventItem`` or ``SubEventItemVariation`` with a price set, the
|
||||
subevent's price configuration takes precedence over both the item as well as the variation and replaces the listed price.
|
||||
|
||||
Listed prices are naive prices. Before we actually show them to the user, we need to check if ``TaxRule.price_includes_tax``
|
||||
is set to determine if we need to add tax or subtract tax to get to the taxed price. We then consider the event's
|
||||
``display_net_prices`` setting to figure out which way to present the taxed price in the interface.
|
||||
|
||||
Guarantees on listed prices
|
||||
---------------------------
|
||||
|
||||
One goal of all further logic is that if a user sees a listed price, they are guaranteed to get the product at that
|
||||
price as long as they complete their purchase within the cart expiration time frame. For example, if the cart expiration
|
||||
time is set to 30 minutes and someone puts a item listed at €23 in their cart at 4pm, they can still complete checkout
|
||||
at €23 until 4.30pm, even if the organizer decides to raise the price to €25 at 4.10pm. If they complete checkout after
|
||||
4.30pm, their cart will be adjusted to the new price and the user will see a warning that the price has changed.
|
||||
|
||||
Computation of cart prices
|
||||
--------------------------
|
||||
|
||||
Input
|
||||
"""""
|
||||
|
||||
To ensure the guarantee mentioned above, even in the light of all possible dynamic changes, the ``listed_price``
|
||||
is explicitly stored in the ``CartPosition`` model after the item has been added to the cart.
|
||||
|
||||
If ``Item.free_price`` is set, the user is allowed to voluntarily increase the price. In this case, the user's input
|
||||
is stored as ``custom_price_input`` without much further validation for use further down below in the process.
|
||||
If ``display_net_prices`` is set, the user's input is also considered to be a net price and ``custom_price_input_is_net``
|
||||
is stored for the cart position. In any other case, the user's input is considered to be a gross price based on the tax
|
||||
rules' default tax rate.
|
||||
|
||||
The computation of prices in the cart always starts from the ``listed_price``. The ``list_price`` is only computed
|
||||
when adding the product to the cart or when extending the cart's lifetime after it expired. All other steps such as
|
||||
creating an order based on the cart trust ``list_price`` without further checks.
|
||||
|
||||
Vouchers
|
||||
""""""""
|
||||
|
||||
As a first step, the cart is checked for any voucher that should be applied to the position. If such a voucher exists,
|
||||
it's discount (percentage or fixed) is applied to the listed price. The result of this is stored to ``price_after_voucher``.
|
||||
Since ``listed_price`` naive, ``price_after_voucher`` is naive as well. As a consequence, if you have a voucher configured
|
||||
to "set the price to €10", it depends on ``TaxRule.price_includes_tax`` again whether this is €10 including or excluding
|
||||
taxes.
|
||||
|
||||
The ``price_after_voucher`` is only computed when adding the product to the cart or when extending the cart's
|
||||
lifetime after it expired. It is also checked again when the order is created, since the available discount might have
|
||||
changed due to the voucher's budget being (almost) exhausted.
|
||||
|
||||
Line price
|
||||
""""""""""
|
||||
|
||||
The next step computes the final price of this position if it is the only position in the cart. This happens in "reverse
|
||||
order", i.e. before the computation can be performed for a cart position, the step needs to be performed on all of its
|
||||
bundled positions. The sum of ``price_after_voucher`` of all bundled positions is now called ``bundled_sum``.
|
||||
|
||||
First, the value from ``price_after_voucher`` will be processed by the applicable ``TaxRule.tax()`` (which is complex
|
||||
in itself but is not documented here in detail at the moment).
|
||||
|
||||
If ``custom_price_input`` is not set, ``bundled_sum`` will be subtracted from the gross price and the net price is
|
||||
adjusted accordingly. The result is stored as ``tax_rate`` and ``line_price_gross`` in the cart position.
|
||||
|
||||
If ``custom_price_input`` is set, the value will be compared to either the gross or the net value of the ``tax()``
|
||||
result, depending on ``custom_price_input_is_net``. If the comparison yields that the custom price is higher, ``tax()``
|
||||
will be called again . Then, ``bundled_sum`` will be subtracted from the gross price and the result is stored like
|
||||
above.
|
||||
|
||||
The computation of ``line_price_gross`` from ``price_after_voucher``, ``custom_price_input``, and tax settings
|
||||
is repeated after every change of anything in the cart or after every change of the invoice address.
|
||||
|
||||
Discounts
|
||||
---------
|
||||
|
||||
After ``line_price_gross`` has been computed for all positions, the discount engine will run to apply any automatic
|
||||
discounts. Organizers can add rules for automatic discounts in the pretix backend. These rules are ordered and
|
||||
will be applied in order. Every cart position can only be "used" by one discount rule. "Used" can either mean that
|
||||
the price of the position was actually discounted, but it can also mean that the position was required to enable
|
||||
a discount for a different position, e.g. in case of a "buy 3 for the price of 2" offer.
|
||||
|
||||
The algorithm for applying an individual discount rule first starts with eliminating all products that do not match
|
||||
the rule based on its product scope. Then, the algorithm is handled differently for different configurations.
|
||||
|
||||
Case 1: Discount based on minimum value without respect to subevents
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
* Check whether the gross sum of all positions is at least ``condition_min_value``, otherwise abort.
|
||||
|
||||
* Reduce the price of all positions by ``benefit_discount_matching_percent``.
|
||||
|
||||
* Mark all positions as "used" to hide them from further rules
|
||||
|
||||
Case 2: Discount based on minimum number of tickets without respect to subevents
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
* Check whether the number of all positions is at least ``condition_min_count``, otherwise abort.
|
||||
|
||||
* If ``benefit_only_apply_to_cheapest_n_maches`` is set,
|
||||
|
||||
* Sort all positions by price.
|
||||
* Reduce the price of the first ``n_positions // condition_min_count * benefit_only_apply_to_cheapest_n_matches`` positions by ``benefit_discount_matching_percent``.
|
||||
* Mark the first ``n_positions // condition_min_count * condition_min_count`` as "used" to hide them from further rules.
|
||||
* Mark all positions as "used" to hide them from further rules.
|
||||
|
||||
* Else,
|
||||
|
||||
* Reduce the price of all positions by ``benefit_discount_matching_percent``.
|
||||
* Mark all positions as "used" to hide them from further rules.
|
||||
|
||||
Case 3: Discount only for products of the same subevent
|
||||
"""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
* Split the cart into groups based on the subevent.
|
||||
|
||||
* Proceed with case 1 or 2 for every group.
|
||||
|
||||
Case 4: Discount only for products of distinct subevents
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
* Let ``subevents`` be a list of distinct subevents in the cart.
|
||||
|
||||
* Let ``positions[subevent]`` be a list of positions for every subevent.
|
||||
|
||||
* Let ``current_group`` be the current group and ``groups`` the list of all groups.
|
||||
|
||||
* Repeat
|
||||
|
||||
* Order ``subevents`` by the length of their ``positions[subevent]`` list, starting with the longest list.
|
||||
Do not count positions that are part of ``current_group`` already.
|
||||
|
||||
* Let ``candidates`` be the concatenation of all ``positions[subevent]`` lists with the same length as the
|
||||
longest list.
|
||||
|
||||
* If ``candidates`` is empty, abort the repetition.
|
||||
|
||||
* Order ``candidates`` by their price, starting with the lowest price.
|
||||
|
||||
* Pick one entry from ``candidates`` and put it into ``current_group``. If ``current_group`` is shorter than
|
||||
``benefit_only_apply_to_cheapest_n_matches``, we pick from the start (lowest price), otherwise we pick from
|
||||
the end (highest price)
|
||||
|
||||
* If ``current_group`` is now ``condition_min_count``, remove all entries from ``current_group`` from
|
||||
``positions[…]``, add ``current_group`` to ``groups``, and reset ``current_group`` to an empty group.
|
||||
|
||||
* For every position still left in a ``positions[…]`` list, try if there is any ``group`` in groups that it can
|
||||
still be added to without violating the rule of distinct subevents
|
||||
|
||||
* For every group in ``groups``, proceed with case 1 or 2.
|
||||
|
||||
Flowchart
|
||||
---------
|
||||
|
||||
.. image:: /images/cart_pricing.png
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB |
@@ -1,28 +0,0 @@
|
||||
@startuml
|
||||
|
||||
partition "For every cart position" {
|
||||
(*) --> "Get default price from product"
|
||||
--> if "Product has variations?" then
|
||||
-->[yes] "Override with price from variation"
|
||||
--> if "Event series?" then
|
||||
-->[yes] "Override with price from subevent"
|
||||
-down-> "Store as listed_price"
|
||||
else
|
||||
-down->[no] "Store as listed_price"
|
||||
endif
|
||||
else
|
||||
-down->[no] "Store as listed_price"
|
||||
endif
|
||||
--> if "Voucher applied?" then
|
||||
-->[yes] "Apply voucher pricing"
|
||||
--> "Store as price_after_voucher"
|
||||
else
|
||||
-->[no] "Store as price_after_voucher"
|
||||
endif
|
||||
--> "Apply custom price if product allows\nApply tax rule\nSubtract bundled products"
|
||||
--> "Store as line_price (gross), tax_rate"
|
||||
}
|
||||
--> "Apply discount engine"
|
||||
--> "Store as price (gross)"
|
||||
|
||||
@enduml
|
||||
@@ -52,7 +52,6 @@ Variable Description
|
||||
``order_email`` E-mail address of the ticket purchaser
|
||||
``product_id`` Internal ID of the purchased product
|
||||
``product_variation`` Internal ID of the purchased product variation (or empty)
|
||||
``secret`` The secret ticket code, would be used as the QR code for physical tickets
|
||||
``attendee_name`` Full name of the ticket holder (or empty)
|
||||
``attendee_name_*`` Name parts of the ticket holder, depending on configuration, e.g. ``attendee_name_given_name`` or ``attendee_name_family_name``
|
||||
``attendee_email`` E-mail address of the ticket holder (or empty)
|
||||
|
||||
@@ -1,44 +1,20 @@
|
||||
Use case: Group discounts
|
||||
-------------------------
|
||||
|
||||
Often times, you want to give discounts for whole groups attending your event.
|
||||
Often times, you want to give discounts for whole groups attending your event. pretix can't automatically discount based on volume, but there's still some ways you can set up group tickets.
|
||||
|
||||
Automatic discounts
|
||||
"""""""""""""""""""
|
||||
|
||||
pretix can automatically grant discounts if a certain condition is met, such as a specific group size. To set this up,
|
||||
head to **Products**, **Discounts** in the event navigation and **Create a new discount**. You can choose a name so you
|
||||
can later find this again. You can also optionally restrict the discount to a specific time frame or a specific sales
|
||||
channel.
|
||||
|
||||
Next, either select **Apply to all products** or create a selection of products that are eligible for the discount.
|
||||
|
||||
For a **percentual group discount** similar to "if you buy at least 5 tickets, you get 20 percent off", set
|
||||
**Minimum number of matching products** to "5" and **Percentual discount on matching products** to "20.00".
|
||||
|
||||
For a **buy-X-get-Y discount**, e.g. "if you buy 5 tickets, you get one free", set
|
||||
**Minimum number of matching products** to "5", **Percentual discount on matching products** to "100.00", and
|
||||
**Apply discount only to this number of matching products** to "1".
|
||||
|
||||
Fixed group packages
|
||||
Flexible group sizes
|
||||
""""""""""""""""""""
|
||||
|
||||
If you want to sell group tickets in fixed sizes, e.g. a table of eight at your gala dinner, you can use product bundles.
|
||||
Assuming you already set up a ticket for admission of single persons, you then set up a second product **Table (8 persons)**
|
||||
with a discounted full price. Then, head to the **Bundled products** tab of that product and add one bundle configuration
|
||||
to include the single admission product **eight times**. Next, create an unlimited quota mapped to the new product.
|
||||
|
||||
This way, the purchase of a table will automatically create eight tickets, leading to a correct calculation of your total
|
||||
quota and, as expected, eight persons on your check-in list. You can even ask for the individual names of the persons
|
||||
during checkout.
|
||||
|
||||
Minimum order amount
|
||||
""""""""""""""""""""
|
||||
|
||||
If you want to promote discounted group tickets in your price list, you can also do so by creating a special
|
||||
**Group ticket** at the reduced per-person price and set the **Minimum amount per order** option of the ticket to the minimal
|
||||
group size.
|
||||
If you want to give out discounted tickets to groups starting at a given size, but still billed per person, you can do so by creating a special **Group ticket** at the per-person price and set the **Minimum amount per order** option of the ticket to the minimal group size.
|
||||
|
||||
For more complex use cases, you can also use add-on products that can be chosen multiple times.
|
||||
|
||||
This way, your ticket can be bought an arbitrary number of times – but no less than the given minimal amount per order.
|
||||
|
||||
Fixed group sizes
|
||||
"""""""""""""""""
|
||||
|
||||
If you want to sell group tickets in fixed sizes, e.g. a table of eight at your gala dinner, you can use product bundles. Assuming you already set up a ticket for admission of single persons, you then set up a second product **Table (8 persons)** with a discounted full price. Then, head to the **Bundled products** tab of that product and add one bundle configuration to include the single admission product **eight times**. Next, create an unlimited quota mapped to the new product.
|
||||
|
||||
This way, the purchase of a table will automatically create eight tickets, leading to a correct calculation of your total quota and, as expected, eight persons on your check-in list. You can even ask for the individual names of the persons during checkout.
|
||||
|
||||
@@ -84,9 +84,7 @@ going to develop around pretix, for example connect to pretix through our API, y
|
||||
- A voucher is a code that can be used for multiple purposes: To grant a discount to specific customers, to only
|
||||
show certain products to certain customers, or to keep a seat open for someone specific even though you are
|
||||
sold out. If a voucher is used to apply a discount, the price of the purchased product is reduced by the
|
||||
* - | |:gb:| **(Automatic) Discount**
|
||||
| |:de:| (Automatischer) Rabatt
|
||||
- Discounts can be used to automatically provide discounts to customers if their cart satisfies a certain condition.
|
||||
discounted amount. Vouchers are connected to a specific event.
|
||||
* - | |:gb:| **Gift card**
|
||||
| |:de:| Wertgutschein
|
||||
- A :ref:`gift card <giftcards>` is a coupon representing an exact amount of money that can be used for purchases
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "4.10.1"
|
||||
__version__ = "4.9.1"
|
||||
|
||||
@@ -151,8 +151,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:ticketlayoutitem-list'),
|
||||
('GET', 'api-v1:badgelayout-list'),
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:voucher-list'),
|
||||
('GET', 'api-v1:voucher-detail'),
|
||||
('GET', 'api-v1:order-list'),
|
||||
('POST', 'api-v1:order-list'),
|
||||
('GET', 'api-v1:order-detail'),
|
||||
|
||||
@@ -23,7 +23,6 @@ import os
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.files import File
|
||||
from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy
|
||||
@@ -34,19 +33,13 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
||||
)
|
||||
from pretix.base.models import Quota, Seat, Voucher
|
||||
from pretix.base.models import Quota, Seat
|
||||
from pretix.base.models.orders import CartPosition
|
||||
|
||||
|
||||
class TaxIncludedField(serializers.Field):
|
||||
def to_representation(self, instance: CartPosition):
|
||||
return not instance.custom_price_input_is_net
|
||||
|
||||
|
||||
class CartPositionSerializer(I18nAwareModelSerializer):
|
||||
answers = AnswerSerializer(many=True)
|
||||
seat = InlineSeatSerializer()
|
||||
includes_tax = TaxIncludedField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
@@ -61,13 +54,11 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
sales_channel = serializers.CharField(required=False, default='sales_channel')
|
||||
includes_tax = serializers.BooleanField(required=False, allow_null=True)
|
||||
voucher = serializers.CharField(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'subevent', 'expires', 'includes_tax', 'answers', 'seat', 'sales_channel', 'voucher')
|
||||
'subevent', 'expires', 'includes_tax', 'answers', 'seat', 'sales_channel')
|
||||
|
||||
def create(self, validated_data):
|
||||
answers_data = validated_data.pop('answers')
|
||||
@@ -127,53 +118,15 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError('The specified seat ID is not unique.')
|
||||
else:
|
||||
validated_data['seat'] = seat
|
||||
if not seat.is_available(
|
||||
sales_channel=validated_data.get('sales_channel', 'web'),
|
||||
distance_ignore_cart_id=validated_data['cart_id'],
|
||||
):
|
||||
raise ValidationError(gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||
elif seated:
|
||||
raise ValidationError('The specified product requires to choose a seat.')
|
||||
|
||||
if validated_data.get('voucher'):
|
||||
try:
|
||||
voucher = self.context['event'].vouchers.get(code__iexact=validated_data.get('voucher'))
|
||||
except Voucher.DoesNotExist:
|
||||
raise ValidationError('The specified voucher does not exist.')
|
||||
|
||||
if voucher and not voucher.applies_to(validated_data.get('item'), validated_data.get('variation')):
|
||||
raise ValidationError('The specified voucher is not valid for the given item and variation.')
|
||||
|
||||
if voucher and voucher.seat and voucher.seat != validated_data.get('seat'):
|
||||
raise ValidationError('The specified voucher is not valid for this seat.')
|
||||
|
||||
if voucher and voucher.subevent_id and (not validated_data.get('subevent') or voucher.subevent_id != validated_data['subevent'].pk):
|
||||
raise ValidationError('The specified voucher is not valid for this subevent.')
|
||||
|
||||
if voucher.valid_until is not None and voucher.valid_until < now():
|
||||
raise ValidationError('The specified voucher is expired.')
|
||||
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=voucher) & Q(event=self.context['event']) & Q(expires__gte=now())
|
||||
)
|
||||
cart_count = redeemed_in_carts.count()
|
||||
v_avail = voucher.max_usages - voucher.redeemed - cart_count
|
||||
if v_avail < 1:
|
||||
raise ValidationError('The specified voucher has already been used the maximum number of times.')
|
||||
|
||||
validated_data['voucher'] = voucher
|
||||
|
||||
if validated_data.get('seat'):
|
||||
if not validated_data['seat'].is_available(
|
||||
sales_channel=validated_data.get('sales_channel', 'web'),
|
||||
distance_ignore_cart_id=validated_data['cart_id'],
|
||||
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
|
||||
):
|
||||
raise ValidationError(
|
||||
gettext_lazy('The selected seat "{seat}" is not available.').format(seat=validated_data['seat'].name))
|
||||
|
||||
validated_data.pop('sales_channel')
|
||||
# todo: does this make sense?
|
||||
validated_data['custom_price_input'] = validated_data['price']
|
||||
# todo: listed price, etc?
|
||||
# currently does not matter because there is no way to transform an API cart position into an order that keeps
|
||||
# prices, cart positions are just quota/voucher placeholders
|
||||
validated_data['custom_price_input_is_net'] = not validated_data.pop('includes_tax', True)
|
||||
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||
|
||||
for answ_data in answers_data:
|
||||
|
||||
@@ -1,49 +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 pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Discount
|
||||
|
||||
|
||||
class DiscountSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Discount
|
||||
fields = ('id', 'active', 'internal_name', 'position', 'sales_channels', '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',
|
||||
'condition_ignore_voucher_discounted')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
Discount.validate_config(full_data)
|
||||
|
||||
return data
|
||||
@@ -56,9 +56,7 @@ from pretix.base.models.orders import (
|
||||
from pretix.base.pdf import get_images, get_variables
|
||||
from pretix.base.services.cart import error_messages
|
||||
from pretix.base.services.locking import NoLockManager
|
||||
from pretix.base.services.pricing import (
|
||||
apply_discounts, get_line_price, get_listed_price, is_included_for_free,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
@@ -1042,18 +1040,29 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
if v.budget is not None:
|
||||
price = pos_data.get('price')
|
||||
listed_price = get_listed_price(pos_data.get('item'), pos_data.get('variation'), pos_data.get('subevent'))
|
||||
|
||||
if pos_data.get('voucher'):
|
||||
price_after_voucher = pos_data.get('voucher').calculate_price(listed_price)
|
||||
else:
|
||||
price_after_voucher = listed_price
|
||||
if price is None:
|
||||
price = price_after_voucher
|
||||
price = get_price(
|
||||
item=pos_data.get('item'),
|
||||
variation=pos_data.get('variation'),
|
||||
voucher=v,
|
||||
custom_price=None,
|
||||
subevent=pos_data.get('subevent'),
|
||||
addon_to=pos_data.get('addon_to'),
|
||||
invoice_address=ia,
|
||||
).gross
|
||||
pbv = get_price(
|
||||
item=pos_data['item'],
|
||||
variation=pos_data.get('variation'),
|
||||
voucher=None,
|
||||
custom_price=None,
|
||||
subevent=pos_data.get('subevent'),
|
||||
addon_to=pos_data.get('addon_to'),
|
||||
invoice_address=ia,
|
||||
)
|
||||
|
||||
if v not in v_budget:
|
||||
v_budget[v] = v.budget - v.budget_used()
|
||||
disc = max(listed_price - price, 0)
|
||||
disc = pbv.gross - price
|
||||
if disc > v_budget[v]:
|
||||
new_disc = v_budget[v]
|
||||
v_budget[v] -= new_disc
|
||||
@@ -1149,85 +1158,52 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
order.invoice_address = ia
|
||||
ia.last_modified = now()
|
||||
|
||||
# Generate position objects
|
||||
pos_map = {}
|
||||
for pos_data in positions_data:
|
||||
answers_data = pos_data.pop('answers', [])
|
||||
addon_to = pos_data.pop('addon_to', None)
|
||||
attendee_name = pos_data.pop('attendee_name', '')
|
||||
if attendee_name and not pos_data.get('attendee_name_parts'):
|
||||
pos_data['attendee_name_parts'] = {
|
||||
'_legacy': attendee_name
|
||||
}
|
||||
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers'})
|
||||
pos = OrderPosition(**pos_data)
|
||||
if simulate:
|
||||
pos.order = order._wrapped
|
||||
else:
|
||||
pos.order = order
|
||||
if addon_to:
|
||||
if simulate:
|
||||
pos.addon_to = pos_map[addon_to]
|
||||
pos.addon_to = pos_map[addon_to]._wrapped
|
||||
else:
|
||||
pos.addon_to = pos_map[addon_to]
|
||||
|
||||
pos_map[pos.positionid] = pos
|
||||
pos_data['__instance'] = pos
|
||||
|
||||
# Calculate prices if not set
|
||||
for pos_data in positions_data:
|
||||
pos = pos_data['__instance']
|
||||
if pos.addon_to_id and is_included_for_free(pos.item, pos.addon_to):
|
||||
listed_price = Decimal('0.00')
|
||||
else:
|
||||
listed_price = get_listed_price(pos.item, pos.variation, pos.subevent)
|
||||
|
||||
if pos.price is None:
|
||||
if pos.voucher:
|
||||
price_after_voucher = pos.voucher.calculate_price(listed_price)
|
||||
else:
|
||||
price_after_voucher = listed_price
|
||||
|
||||
line_price = get_line_price(
|
||||
price_after_voucher=price_after_voucher,
|
||||
custom_price_input=None,
|
||||
custom_price_input_is_net=False,
|
||||
tax_rule=pos.item.tax_rule,
|
||||
price = get_price(
|
||||
item=pos.item,
|
||||
variation=pos.variation,
|
||||
voucher=pos.voucher,
|
||||
custom_price=None,
|
||||
subevent=pos.subevent,
|
||||
addon_to=pos.addon_to,
|
||||
invoice_address=ia,
|
||||
bundled_sum=Decimal('0.00'),
|
||||
)
|
||||
pos.price = line_price.gross
|
||||
pos._auto_generated_price = True
|
||||
pos.price = price.gross
|
||||
pos.tax_rate = price.rate
|
||||
pos.tax_value = price.tax
|
||||
pos.tax_rule = pos.item.tax_rule
|
||||
else:
|
||||
if pos.voucher:
|
||||
if not pos.item.tax_rule or pos.item.tax_rule.price_includes_tax:
|
||||
price_after_voucher = max(pos.price, pos.voucher.calculate_price(listed_price))
|
||||
else:
|
||||
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
|
||||
else:
|
||||
price_after_voucher = listed_price
|
||||
pos._auto_generated_price = False
|
||||
pos._voucher_discount = listed_price - price_after_voucher
|
||||
if pos.voucher:
|
||||
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
||||
pos._calculate_tax()
|
||||
|
||||
order_positions = [pos_data['__instance'] for pos_data in positions_data]
|
||||
discount_results = apply_discounts(
|
||||
self.context['event'],
|
||||
order.sales_channel,
|
||||
[
|
||||
(cp.item_id, cp.subevent_id, cp.price, bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
||||
for cp in order_positions
|
||||
]
|
||||
)
|
||||
for cp, (new_price, discount) in zip(order_positions, discount_results):
|
||||
if new_price != pos.price and pos._auto_generated_price:
|
||||
pos.price = new_price
|
||||
pos.discount = discount
|
||||
|
||||
# Save instances
|
||||
for pos_data in positions_data:
|
||||
answers_data = pos_data.pop('answers', [])
|
||||
pos = pos_data['__instance']
|
||||
pos._calculate_tax()
|
||||
pos.price_before_voucher = get_price(
|
||||
item=pos.item,
|
||||
variation=pos.variation,
|
||||
voucher=None,
|
||||
custom_price=None,
|
||||
subevent=pos.subevent,
|
||||
addon_to=pos.addon_to,
|
||||
invoice_address=ia,
|
||||
).gross
|
||||
|
||||
if simulate:
|
||||
pos = WrappedModel(pos)
|
||||
@@ -1240,7 +1216,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
answers.append(answ)
|
||||
pos.answers = answers
|
||||
pos.pseudonymization_id = "PREVIEW"
|
||||
pos_map[pos.positionid] = pos
|
||||
else:
|
||||
if pos.voucher:
|
||||
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
||||
@@ -1263,6 +1238,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
else:
|
||||
answ = pos.answers.create(**answ_data)
|
||||
answ.options.add(*options)
|
||||
pos_map[pos.positionid] = pos
|
||||
|
||||
if not simulate:
|
||||
for cp in delete_cps:
|
||||
|
||||
@@ -71,8 +71,8 @@ class CustomerSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = ('identifier', 'external_identifier', 'email', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined',
|
||||
'locale', 'last_modified', 'notes')
|
||||
fields = ('identifier', 'email', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined',
|
||||
'locale', 'last_modified')
|
||||
|
||||
|
||||
class MembershipTypeSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -41,8 +41,8 @@ from rest_framework import routers
|
||||
from pretix.api.views import cart
|
||||
|
||||
from .views import (
|
||||
checkin, device, discount, event, exporters, idempotency, item, oauth,
|
||||
order, organizer, upload, user, version, voucher, waitinglist, webhooks,
|
||||
checkin, device, event, exporters, item, oauth, order, organizer, upload,
|
||||
user, version, voucher, waitinglist, webhooks,
|
||||
)
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
@@ -72,7 +72,6 @@ event_router.register(r'clone', event.CloneEventViewSet)
|
||||
event_router.register(r'items', item.ItemViewSet)
|
||||
event_router.register(r'categories', item.ItemCategoryViewSet)
|
||||
event_router.register(r'questions', item.QuestionViewSet)
|
||||
event_router.register(r'discounts', discount.DiscountViewSet)
|
||||
event_router.register(r'quotas', item.QuotaViewSet)
|
||||
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||
event_router.register(r'orders', order.OrderViewSet)
|
||||
@@ -133,7 +132,6 @@ urlpatterns = [
|
||||
re_path(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
||||
re_path(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
||||
re_path(r"^device/eventselection$", device.EventSelectionView.as_view(), name="device.eventselection"),
|
||||
re_path(r"^idempotency_query$", idempotency.IdempotencyQueryView.as_view(), name="idempotency.query"),
|
||||
re_path(r"^upload$", upload.UploadView.as_view(), name="upload"),
|
||||
re_path(r"^me$", user.MeView.as_view(), name="user.me"),
|
||||
re_path(r"^version$", version.VersionView.as_view(), name="version"),
|
||||
|
||||
@@ -157,7 +157,6 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
list=self.get_object(),
|
||||
successful=False,
|
||||
forced=True,
|
||||
force_sent=True,
|
||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
||||
gate=self.request.auth.gate if isinstance(self.request.auth, Device) else None,
|
||||
**kwargs,
|
||||
@@ -430,7 +429,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
forced=force,
|
||||
)
|
||||
raw_barcode_for_checkin = None
|
||||
from_revoked_secret = False
|
||||
|
||||
try:
|
||||
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
|
||||
@@ -506,7 +504,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
elif revoked_matches and force:
|
||||
op = revoked_matches[0].position
|
||||
raw_barcode_for_checkin = self.kwargs['pk']
|
||||
from_revoked_secret = True
|
||||
else:
|
||||
op = revoked_matches[0].position
|
||||
op.order.log_action('pretix.event.checkin.revoked', data={
|
||||
@@ -558,7 +555,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
auth=self.request.auth,
|
||||
type=type,
|
||||
raw_barcode=raw_barcode_for_checkin,
|
||||
from_revoked_secret=from_revoked_secret,
|
||||
from_revoked_secret=True,
|
||||
)
|
||||
except RequiredQuestionsError as e:
|
||||
return Response({
|
||||
|
||||
@@ -1,99 +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/>.
|
||||
#
|
||||
|
||||
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
||||
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
||||
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
||||
#
|
||||
# This file contains Apache-licensed contributions copyrighted by: Ture Gjørup
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.filters import OrderingFilter
|
||||
|
||||
from pretix.api.serializers.discount import DiscountSerializer
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import CartPosition, Discount
|
||||
|
||||
with scopes_disabled():
|
||||
class DiscountFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Discount
|
||||
fields = ['active']
|
||||
|
||||
|
||||
class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = DiscountSerializer
|
||||
queryset = Discount.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
filterset_class = DiscountFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.discounts.all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.discount.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.discount.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('You cannot delete this discount because it already has '
|
||||
'been used as part of an order.')
|
||||
|
||||
instance.log_action(
|
||||
'pretix.event.discount.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
CartPosition.objects.filter(discount=instance).update(discount=None)
|
||||
super().perform_destroy(instance)
|
||||
@@ -1,80 +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/>.
|
||||
#
|
||||
import json
|
||||
import logging
|
||||
from hashlib import sha1
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from pretix.api.models import ApiCall
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IdempotencyQueryView(APIView):
|
||||
# Experimental feature, therefore undocumented for now
|
||||
authentication_classes = ()
|
||||
permission_classes = ()
|
||||
|
||||
def get(self, request, format=None):
|
||||
idempotency_key = request.GET.get("key")
|
||||
auth_hash_parts = '{}:{}'.format(
|
||||
request.headers.get('Authorization', ''),
|
||||
request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
|
||||
)
|
||||
auth_hash = sha1(auth_hash_parts.encode()).hexdigest()
|
||||
if not idempotency_key:
|
||||
return JsonResponse({
|
||||
'detail': 'No idempotency key given.'
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
try:
|
||||
call = ApiCall.objects.get(
|
||||
auth_hash=auth_hash,
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
except ApiCall.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'detail': 'Idempotency key not seen before.'
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if call.locked:
|
||||
r = JsonResponse(
|
||||
{'detail': 'Concurrent request with idempotency key.'},
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
r['Retry-After'] = 5
|
||||
return r
|
||||
|
||||
content = call.response_body
|
||||
if isinstance(content, memoryview):
|
||||
content = content.tobytes()
|
||||
r = HttpResponse(
|
||||
content=content,
|
||||
status=call.response_code,
|
||||
)
|
||||
for k, v in json.loads(call.response_headers).values():
|
||||
r[k] = v
|
||||
return r
|
||||
@@ -261,11 +261,8 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
provider = self._get_output_provider(output)
|
||||
order = self.get_object()
|
||||
|
||||
if order.status in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
|
||||
raise PermissionDenied("Downloads are not available for canceled or expired orders.")
|
||||
|
||||
if order.status == Order.STATUS_PENDING and not request.event.settings.ticket_download_pending:
|
||||
raise PermissionDenied("Downloads are not available for pending orders.")
|
||||
if order.status != Order.STATUS_PAID:
|
||||
raise PermissionDenied("Downloads are not available for unpaid orders.")
|
||||
|
||||
ct = CachedCombinedTicket.objects.filter(
|
||||
order=order, provider=provider.identifier, file__isnull=False
|
||||
@@ -1122,11 +1119,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
||||
provider = self._get_output_provider(output)
|
||||
pos = self.get_object()
|
||||
|
||||
if pos.order.status in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
|
||||
raise PermissionDenied("Downloads are not available for canceled or expired orders.")
|
||||
|
||||
if pos.order.status == Order.STATUS_PENDING and not request.event.settings.ticket_download_pending:
|
||||
raise PermissionDenied("Downloads are not available for pending orders.")
|
||||
if pos.order.status != Order.STATUS_PAID:
|
||||
raise PermissionDenied("Downloads are not available for unpaid orders.")
|
||||
if not pos.generate_ticket:
|
||||
raise PermissionDenied("Downloads are not enabled for this product.")
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from django.db import transaction
|
||||
from django.db.models import F, Q
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import (
|
||||
BooleanFilter, CharFilter, DjangoFilterBackend, FilterSet,
|
||||
BooleanFilter, DjangoFilterBackend, FilterSet,
|
||||
)
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import status, viewsets
|
||||
@@ -40,7 +40,6 @@ from pretix.base.models import Voucher
|
||||
with scopes_disabled():
|
||||
class VoucherFilter(FilterSet):
|
||||
active = BooleanFilter(method='filter_active')
|
||||
code = CharFilter(lookup_expr='iexact')
|
||||
|
||||
class Meta:
|
||||
model = Voucher
|
||||
|
||||
@@ -89,13 +89,6 @@ class SalesChannel:
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def discounts_supported(self) -> bool:
|
||||
"""
|
||||
If this property is ``True``, this sales channel can be selected for automatic discounts.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
def get_all_sales_channels():
|
||||
global _ALL_CHANNELS
|
||||
|
||||
@@ -259,7 +259,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
payment_providers=Subquery(p_providers, output_field=CharField()),
|
||||
invoice_numbers=Subquery(i_numbers, output_field=CharField()),
|
||||
pcnt=Subquery(s, output_field=IntegerField())
|
||||
).select_related('invoice_address', 'customer')
|
||||
).select_related('invoice_address')
|
||||
|
||||
qs = self._date_filter(qs, form_data, rel='')
|
||||
|
||||
@@ -268,8 +268,8 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
tax_rates = self._get_all_tax_rates(qs)
|
||||
|
||||
headers = [
|
||||
_('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Phone number'),
|
||||
_('Order date'), _('Order time'), _('Company'), _('Name'),
|
||||
_('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Phone number'), _('Order date'),
|
||||
_('Order time'), _('Company'), _('Name'),
|
||||
]
|
||||
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
|
||||
if name_scheme and len(name_scheme['fields']) > 1:
|
||||
@@ -294,7 +294,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(_('Follow-up date'))
|
||||
headers.append(_('Positions'))
|
||||
headers.append(_('E-mail address verified'))
|
||||
headers.append(_('External customer ID'))
|
||||
headers.append(_('Payment providers'))
|
||||
if form_data.get('include_payment_amounts'):
|
||||
payment_methods = self._get_all_payment_methods(qs)
|
||||
@@ -401,7 +400,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
||||
row.append(order.pcnt)
|
||||
row.append(_('Yes') if order.email_known_to_work else _('No'))
|
||||
row.append(str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '')
|
||||
row.append(', '.join([
|
||||
str(self.providers.get(p, p)) for p in sorted(set((order.payment_providers or '').split(',')))
|
||||
if p and p != 'free'
|
||||
@@ -426,13 +424,13 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
).values(
|
||||
'm'
|
||||
).order_by()
|
||||
qs = OrderFee.all.filter(
|
||||
qs = OrderFee.objects.filter(
|
||||
order__event__in=self.events,
|
||||
).annotate(
|
||||
payment_providers=Subquery(p_providers, output_field=CharField()),
|
||||
).select_related('order', 'order__invoice_address', 'order__customer', 'tax_rule')
|
||||
).select_related('order', 'order__invoice_address', 'tax_rule')
|
||||
if form_data['paid_only']:
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID)
|
||||
|
||||
qs = self._date_filter(qs, form_data, rel='order__')
|
||||
|
||||
@@ -461,7 +459,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
|
||||
]
|
||||
|
||||
headers.append(_('External customer ID'))
|
||||
headers.append(_('Payment providers'))
|
||||
yield headers
|
||||
|
||||
@@ -472,7 +469,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row = [
|
||||
self.event_object_cache[order.event_id].slug,
|
||||
order.code,
|
||||
_("canceled") if op.canceled else order.get_status_display(),
|
||||
order.get_status_display(),
|
||||
order.email,
|
||||
str(order.phone) if order.phone else '',
|
||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
@@ -505,7 +502,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
]
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0))
|
||||
row.append(str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '')
|
||||
row.append(', '.join([
|
||||
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
|
||||
if p and p != 'free'
|
||||
@@ -522,19 +518,19 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
).values(
|
||||
'm'
|
||||
).order_by()
|
||||
base_qs = OrderPosition.all.filter(
|
||||
base_qs = OrderPosition.objects.filter(
|
||||
order__event__in=self.events,
|
||||
)
|
||||
qs = base_qs.annotate(
|
||||
payment_providers=Subquery(p_providers, output_field=CharField()),
|
||||
).select_related(
|
||||
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
|
||||
'order', 'order__invoice_address', 'item', 'variation',
|
||||
'voucher', 'tax_rule'
|
||||
).prefetch_related(
|
||||
'answers', 'answers__question', 'answers__options'
|
||||
)
|
||||
if form_data['paid_only']:
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID)
|
||||
|
||||
qs = self._date_filter(qs, form_data, rel='order__')
|
||||
|
||||
@@ -615,7 +611,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers += [
|
||||
_('Sales channel'), _('Order locale'),
|
||||
_('E-mail address verified'),
|
||||
_('External customer ID'),
|
||||
_('Payment providers'),
|
||||
]
|
||||
|
||||
@@ -633,7 +628,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
self.event_object_cache[order.event_id].slug,
|
||||
order.code,
|
||||
op.positionid,
|
||||
_("canceled") if op.canceled else order.get_status_display(),
|
||||
order.get_status_display(),
|
||||
order.email,
|
||||
str(order.phone) if order.phone else '',
|
||||
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
|
||||
@@ -735,8 +730,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row += [
|
||||
order.sales_channel,
|
||||
order.locale,
|
||||
_('Yes') if order.email_known_to_work else _('No'),
|
||||
str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '',
|
||||
_('Yes') if order.email_known_to_work else _('No')
|
||||
]
|
||||
row.append(', '.join([
|
||||
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
|
||||
|
||||
@@ -196,16 +196,10 @@ class SecretKeySettingsWidget(forms.TextInput):
|
||||
attrs.update({
|
||||
'autocomplete': 'new-password' # see https://bugs.chromium.org/p/chromium/issues/detail?id=370363#c7
|
||||
})
|
||||
self.__reflect_value = False
|
||||
super().__init__(attrs)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
value = super().value_from_datadict(data, files, name)
|
||||
self.__reflect_value = value and value != SECRET_REDACTED
|
||||
return value
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
if value and not self.__reflect_value:
|
||||
if value:
|
||||
value = SECRET_REDACTED
|
||||
return super().get_context(name, value, attrs)
|
||||
|
||||
|
||||
@@ -429,7 +429,7 @@ class PortraitImageWidget(UploadedFileWidget):
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
d = super().value_from_datadict(data, files, name)
|
||||
if d is not None and d is not False:
|
||||
if d is not None:
|
||||
d._cropdata = json.loads(data.get(name + '_cropdata', '{}') or '{}')
|
||||
return d
|
||||
|
||||
|
||||
@@ -510,7 +510,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
return story
|
||||
|
||||
def _get_story(self, doc):
|
||||
has_taxes = any(il.tax_value for il in self.invoice.lines.all()) or self.invoice.reverse_charge
|
||||
has_taxes = any(il.tax_value for il in self.invoice.lines.all())
|
||||
|
||||
story = [
|
||||
NextPageTemplate('FirstPage'),
|
||||
|
||||
@@ -76,10 +76,6 @@ class LocaleMiddleware(MiddlewareMixin):
|
||||
if lang.startswith(firstpart + '-'):
|
||||
language = lang
|
||||
break
|
||||
if language not in settings_holder.settings.locales:
|
||||
# This seems redundant, but can happen in the rare edge case that settings.locale is (wrongfully)
|
||||
# not part of settings.locales
|
||||
language = settings_holder.settings.locales[0]
|
||||
if '-' not in language and settings_holder.settings.region:
|
||||
language += '-' + settings_holder.settings.region
|
||||
else:
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# Generated by Django 3.2.2 on 2022-03-03 20:17
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.base
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0209_device_info'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='custom_price_input',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='custom_price_input_is_net',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='line_price_gross',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='listed_price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='price_after_voucher',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='tax_rate',
|
||||
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=7),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Discount',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('internal_name', models.CharField(max_length=255)),
|
||||
('position', models.PositiveIntegerField(default=0)),
|
||||
('sales_channels', pretix.base.models.fields.MultiStringField(default=['web'])),
|
||||
('available_from', models.DateTimeField(blank=True, null=True)),
|
||||
('available_until', models.DateTimeField(blank=True, null=True)),
|
||||
('subevent_mode', models.CharField(max_length=50, default='mixed')),
|
||||
('condition_all_products', models.BooleanField(default=True)),
|
||||
('condition_min_count', models.PositiveIntegerField(default=0)),
|
||||
('condition_min_value', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
|
||||
('benefit_discount_matching_percent', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
|
||||
('benefit_only_apply_to_cheapest_n_matches', models.PositiveIntegerField(null=True)),
|
||||
('condition_limit_products', models.ManyToManyField(to='pretixbase.Item')),
|
||||
('condition_apply_to_addons', models.BooleanField(default=True)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='discounts', to='pretixbase.event')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='discount',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, to='pretixbase.discount'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='discount',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, to='pretixbase.discount'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='voucher_budget_use',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 3.2.2 on 2022-03-14 20:01
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.models import F
|
||||
from django.db.models.functions import Greatest
|
||||
|
||||
|
||||
def migrate_voucher_budget_use(apps, schema_editor):
|
||||
OrderPosition = apps.get_model('pretixbase', 'OrderPosition') # noqa
|
||||
OrderPosition.all.filter(
|
||||
price_before_voucher__isnull=False
|
||||
).exclude(price=F('price_before_voucher')).update(
|
||||
voucher_budget_use=Greatest(F('price_before_voucher') - F('price'), Decimal('0.00'))
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0210_auto_20220303_2017'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
migrate_voucher_budget_use,
|
||||
migrations.RunPython.noop,
|
||||
),
|
||||
]
|
||||
@@ -1,29 +0,0 @@
|
||||
# Generated by Django 3.2.12 on 2022-03-18 14:08
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0211_auto_20220314_2001'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='cartposition',
|
||||
name='includes_tax',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cartposition',
|
||||
name='override_tax_rate',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cartposition',
|
||||
name='price_before_voucher',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='orderposition',
|
||||
name='price_before_voucher',
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.2.2 on 2022-04-13 08:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0212_auto_20220318_1408'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='discount',
|
||||
name='condition_ignore_voucher_discounted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 3.2.12 on 2022-04-28 08:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0213_discount_condition_ignore_voucher_discounted'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='external_identifier',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customer',
|
||||
name='notes',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 3.2.12 on 2022-05-12 15:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0214_customer_notes_ext_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='identifier',
|
||||
field=models.CharField(db_index=True, max_length=190),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='customer',
|
||||
unique_together={('organizer', 'email'), ('organizer', 'identifier')},
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.2.12 on 2022-04-29 13:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0215_customer_organizer_identifier_unique'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='force_sent',
|
||||
field=models.BooleanField(default=False, null=True),
|
||||
),
|
||||
]
|
||||
@@ -25,7 +25,6 @@ from .base import CachedFile, LoggedModel, cachedfile_name
|
||||
from .checkin import Checkin, CheckinList
|
||||
from .customers import Customer
|
||||
from .devices import Device, Gate
|
||||
from .discount import Discount
|
||||
from .event import (
|
||||
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
|
||||
SubEvent, SubEventMetaValue, generate_invite_token,
|
||||
|
||||
@@ -326,13 +326,7 @@ class Checkin(models.Model):
|
||||
type = models.CharField(max_length=100, choices=CHECKIN_TYPES, default=TYPE_ENTRY)
|
||||
|
||||
nonce = models.CharField(max_length=190, null=True, blank=True)
|
||||
|
||||
# Whether or not the scan was made offline
|
||||
force_sent = models.BooleanField(default=False, null=True, blank=True)
|
||||
|
||||
# Whether the scan was made offline AND would have not been possible online
|
||||
forced = models.BooleanField(default=False)
|
||||
|
||||
device = models.ForeignKey(
|
||||
'pretixbase.Device', related_name='checkins', on_delete=models.PROTECT, null=True, blank=True
|
||||
)
|
||||
|
||||
@@ -24,7 +24,6 @@ from django.conf import settings
|
||||
from django.contrib.auth.hashers import (
|
||||
check_password, is_password_usable, make_password,
|
||||
)
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.utils.crypto import get_random_string, salted_hmac
|
||||
@@ -45,18 +44,7 @@ class Customer(LoggedModel):
|
||||
"""
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
organizer = models.ForeignKey(Organizer, related_name='customers', on_delete=models.CASCADE)
|
||||
identifier = models.CharField(
|
||||
max_length=190,
|
||||
db_index=True,
|
||||
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
|
||||
'not input one, we will generate one automatically.'),
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex=r"^[a-zA-Z0-9]([a-zA-Z0-9.\-_]*[a-zA-Z0-9])?$",
|
||||
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores. It must start and end with a letter or number."),
|
||||
),
|
||||
],
|
||||
)
|
||||
identifier = models.CharField(max_length=190, db_index=True, unique=True)
|
||||
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('E-mail'), max_length=190)
|
||||
phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number'))
|
||||
password = models.CharField(verbose_name=_('Password'), max_length=128)
|
||||
@@ -71,13 +59,11 @@ class Customer(LoggedModel):
|
||||
default=settings.LANGUAGE_CODE,
|
||||
verbose_name=_('Language'))
|
||||
last_modified = models.DateTimeField(auto_now=True)
|
||||
external_identifier = models.CharField(max_length=255, verbose_name=_('External identifier'), null=True, blank=True)
|
||||
notes = models.TextField(verbose_name=_('Notes'), null=True, blank=True)
|
||||
|
||||
objects = ScopedManager(organizer='organizer')
|
||||
|
||||
class Meta:
|
||||
unique_together = [['organizer', 'email'], ['organizer', 'identifier']]
|
||||
unique_together = [['organizer', 'email']]
|
||||
ordering = ('email',)
|
||||
|
||||
def get_email_field_name(self):
|
||||
@@ -104,8 +90,6 @@ class Customer(LoggedModel):
|
||||
self.name_cached = ''
|
||||
self.email = None
|
||||
self.phone = None
|
||||
self.external_identifier = None
|
||||
self.notes = None
|
||||
self.save()
|
||||
self.all_logentries().update(data={}, shredded=True)
|
||||
self.orders.all().update(customer=None)
|
||||
|
||||
@@ -179,7 +179,6 @@ class Device(LoggedModel):
|
||||
return {
|
||||
'can_view_orders',
|
||||
'can_change_orders',
|
||||
'can_view_vouchers',
|
||||
'can_manage_gift_cards'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,368 +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 collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from itertools import groupby
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import fields
|
||||
from pretix.base.models.base import LoggedModel
|
||||
|
||||
|
||||
class Discount(LoggedModel):
|
||||
SUBEVENT_MODE_MIXED = 'mixed'
|
||||
SUBEVENT_MODE_SAME = 'same'
|
||||
SUBEVENT_MODE_DISTINCT = 'distinct'
|
||||
SUBEVENT_MODE_CHOICES = (
|
||||
(SUBEVENT_MODE_MIXED, pgettext_lazy('subevent', 'Dates can be mixed without limitation')),
|
||||
(SUBEVENT_MODE_SAME, pgettext_lazy('subevent', 'All matching products must be for the same date')),
|
||||
(SUBEVENT_MODE_DISTINCT, pgettext_lazy('subevent', 'Each matching product must be for a different date')),
|
||||
)
|
||||
|
||||
event = models.ForeignKey(
|
||||
'Event',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='discounts',
|
||||
)
|
||||
active = models.BooleanField(
|
||||
verbose_name=_("Active"),
|
||||
default=True,
|
||||
)
|
||||
internal_name = models.CharField(
|
||||
verbose_name=_("Internal name"),
|
||||
max_length=255
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
sales_channels = fields.MultiStringField(
|
||||
verbose_name=_('Sales channels'),
|
||||
default=['web'],
|
||||
blank=False,
|
||||
)
|
||||
|
||||
available_from = models.DateTimeField(
|
||||
verbose_name=_("Available from"),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
available_until = models.DateTimeField(
|
||||
verbose_name=_("Available until"),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
subevent_mode = models.CharField(
|
||||
verbose_name=_('Event series handling'),
|
||||
max_length=50,
|
||||
default=SUBEVENT_MODE_MIXED,
|
||||
choices=SUBEVENT_MODE_CHOICES,
|
||||
)
|
||||
|
||||
condition_all_products = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Apply to all products (including newly created ones)")
|
||||
)
|
||||
condition_limit_products = models.ManyToManyField(
|
||||
'Item',
|
||||
verbose_name=_("Apply to specific products"),
|
||||
blank=True
|
||||
)
|
||||
condition_apply_to_addons = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Apply to add-on products"),
|
||||
help_text=_("Discounts never apply to bundled products"),
|
||||
)
|
||||
condition_ignore_voucher_discounted = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Ignore products discounted by a voucher"),
|
||||
help_text=_("If this option is checked, products that already received a discount through a voucher will not "
|
||||
"be considered for this discount. However, products that use a voucher only to e.g. unlock a "
|
||||
"hidden product or gain access to sold-out quota will still receive the discount."),
|
||||
)
|
||||
condition_min_count = models.PositiveIntegerField(
|
||||
verbose_name=_('Minimum number of matching products'),
|
||||
default=0,
|
||||
)
|
||||
condition_min_value = models.DecimalField(
|
||||
verbose_name=_('Minimum gross value of matching products'),
|
||||
decimal_places=2,
|
||||
max_digits=10,
|
||||
default=Decimal('0.00'),
|
||||
)
|
||||
|
||||
benefit_discount_matching_percent = models.DecimalField(
|
||||
verbose_name=_('Percentual discount on matching products'),
|
||||
decimal_places=2,
|
||||
max_digits=10,
|
||||
default=Decimal('0.00'),
|
||||
validators=[MinValueValidator(Decimal('0.00'))],
|
||||
)
|
||||
benefit_only_apply_to_cheapest_n_matches = models.PositiveIntegerField(
|
||||
verbose_name=_('Apply discount only to this number of matching products'),
|
||||
help_text=_(
|
||||
'This option allows you to create discounts of the type "buy X get Y reduced/for free". For example, if '
|
||||
'you set "Minimum number of matching products" to four and this value to two, the customer\'s cart will be '
|
||||
'split into groups of four tickets and the cheapest two tickets within every group will be discounted. If '
|
||||
'you want to grant the discount on all matching products, keep this field empty.'
|
||||
),
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(1)],
|
||||
)
|
||||
|
||||
# more feature ideas:
|
||||
# - max_usages_per_order
|
||||
# - promote_to_user_if_almost_satisfied
|
||||
# - require_customer_account
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
ordering = ('position', 'id')
|
||||
|
||||
def __str__(self):
|
||||
return self.internal_name
|
||||
|
||||
@property
|
||||
def sortkey(self):
|
||||
return self.position, self.id
|
||||
|
||||
def __lt__(self, other) -> bool:
|
||||
return self.sortkey < other.sortkey
|
||||
|
||||
@classmethod
|
||||
def validate_config(cls, data):
|
||||
# We forbid a few combinations of settings, because we don't think they are neccessary and at the same
|
||||
# time they introduce edge cases, in which it becomes almost impossible to compute the discount optimally
|
||||
# and also very hard to understand for the user what is going on.
|
||||
if data.get('condition_min_count') and data.get('condition_min_value'):
|
||||
raise ValidationError(
|
||||
_('You can either set a minimum number of matching products or a minimum value, not both.')
|
||||
)
|
||||
|
||||
if not data.get('condition_min_count') and not data.get('condition_min_value'):
|
||||
raise ValidationError(
|
||||
_('You need to either set a minimum number of matching products or a minimum value.')
|
||||
)
|
||||
|
||||
if data.get('condition_min_value') and data.get('benefit_only_apply_to_cheapest_n_matches'):
|
||||
raise ValidationError(
|
||||
_('You cannot apply the discount only to some of the matched products if you are matching '
|
||||
'on a minimum value.')
|
||||
)
|
||||
|
||||
if data.get('subevent_mode') == cls.SUBEVENT_MODE_DISTINCT and data.get('condition_min_value'):
|
||||
raise ValidationError(
|
||||
_('You cannot apply the discount only to bookings of different dates if you are matching '
|
||||
'on a minimum value.')
|
||||
)
|
||||
|
||||
def allow_delete(self):
|
||||
return not self.orderposition_set.exists()
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
Discount.validate_config({
|
||||
'condition_min_count': self.condition_min_count,
|
||||
'condition_min_value': self.condition_min_value,
|
||||
'benefit_only_apply_to_cheapest_n_matches': self.benefit_only_apply_to_cheapest_n_matches,
|
||||
'subevent_mode': self.subevent_mode,
|
||||
})
|
||||
|
||||
def _apply_min_value(self, positions, idx_group, result):
|
||||
if self.condition_min_value and sum(positions[idx][2] for idx in idx_group) < self.condition_min_value:
|
||||
return
|
||||
|
||||
if self.condition_min_count or self.benefit_only_apply_to_cheapest_n_matches:
|
||||
raise ValueError('Validation invariant violated.')
|
||||
|
||||
for idx in idx_group:
|
||||
previous_price = positions[idx][2]
|
||||
new_price = round_decimal(
|
||||
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
|
||||
self.event.currency,
|
||||
)
|
||||
result[idx] = new_price
|
||||
|
||||
def _apply_min_count(self, positions, idx_group, result):
|
||||
if len(idx_group) < self.condition_min_count:
|
||||
return
|
||||
|
||||
if not self.condition_min_count or self.condition_min_value:
|
||||
raise ValueError('Validation invariant violated.')
|
||||
|
||||
if self.benefit_only_apply_to_cheapest_n_matches:
|
||||
if not self.condition_min_count:
|
||||
raise ValueError('Validation invariant violated.')
|
||||
|
||||
idx_group = sorted(idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
|
||||
|
||||
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
|
||||
# want to match multiples of 3
|
||||
consume_idx = idx_group[:len(idx_group) // self.condition_min_count * self.condition_min_count]
|
||||
benefit_idx = idx_group[:len(idx_group) // self.condition_min_count * self.benefit_only_apply_to_cheapest_n_matches]
|
||||
else:
|
||||
consume_idx = idx_group
|
||||
benefit_idx = idx_group
|
||||
|
||||
for idx in benefit_idx:
|
||||
previous_price = positions[idx][2]
|
||||
new_price = round_decimal(
|
||||
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
|
||||
self.event.currency,
|
||||
)
|
||||
result[idx] = new_price
|
||||
|
||||
for idx in consume_idx:
|
||||
result.setdefault(idx, positions[idx][2])
|
||||
|
||||
def apply(self, positions: Dict[int, Tuple[int, Optional[int], Decimal, bool, Decimal]]) -> Dict[int, Decimal]:
|
||||
"""
|
||||
Tries to apply this discount to a cart
|
||||
|
||||
:param positions: Dictionary mapping IDs to tuples of the form
|
||||
``(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)``.
|
||||
Bundled positions may not be included.
|
||||
|
||||
:return: A dictionary mapping keys from the input dictionary to new prices. All positions
|
||||
contained in this dictionary are considered "consumed" and should not be considered
|
||||
by other discounts.
|
||||
"""
|
||||
result = {}
|
||||
|
||||
if not self.active:
|
||||
return result
|
||||
|
||||
limit_products = set()
|
||||
if not self.condition_all_products:
|
||||
limit_products = {p.pk for p in self.condition_limit_products.all()}
|
||||
|
||||
# First, filter out everything not even covered by our product scope
|
||||
initial_candidates = [
|
||||
idx
|
||||
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'))
|
||||
)
|
||||
]
|
||||
|
||||
if self.subevent_mode == self.SUBEVENT_MODE_MIXED: # also applies to non-series events
|
||||
if self.condition_min_count:
|
||||
self._apply_min_count(positions, initial_candidates, result)
|
||||
else:
|
||||
self._apply_min_value(positions, initial_candidates, result)
|
||||
|
||||
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
|
||||
def key(idx):
|
||||
return positions[idx][1] # subevent_id
|
||||
|
||||
# Build groups of candidates with the same subevent, then apply our regular algorithm
|
||||
# to each group
|
||||
|
||||
_groups = groupby(sorted(initial_candidates, key=key), key=key)
|
||||
candidate_groups = [list(g) for k, g in _groups]
|
||||
|
||||
for g in candidate_groups:
|
||||
if self.condition_min_count:
|
||||
self._apply_min_count(positions, g, result)
|
||||
else:
|
||||
self._apply_min_value(positions, g, result)
|
||||
|
||||
elif self.subevent_mode == self.SUBEVENT_MODE_DISTINCT:
|
||||
if self.condition_min_value:
|
||||
raise ValueError('Validation invariant violated.')
|
||||
|
||||
# Build optimal groups of candidates with distinct subevents, then apply our regular algorithm
|
||||
# to each group. Optimal, in this case, means:
|
||||
# - First try to build as many groups of size condition_min_count as possible while trying to
|
||||
# balance out the cheapest products so that they are not all in the same group
|
||||
# - Then add remaining positions to existing groups if possible
|
||||
candidate_groups = []
|
||||
|
||||
# Build a list of subevent IDs in descending order of frequency
|
||||
subevent_to_idx = defaultdict(list)
|
||||
for idx, p in positions.items():
|
||||
subevent_to_idx[p[1]].append(idx)
|
||||
for v in subevent_to_idx.values():
|
||||
v.sort(key=lambda idx: positions[idx][2])
|
||||
subevent_order = sorted(list(subevent_to_idx.keys()), key=lambda s: len(subevent_to_idx[s]), reverse=True)
|
||||
|
||||
# Build groups of exactly condition_min_count distinct subevents
|
||||
current_group = []
|
||||
while True:
|
||||
# Build a list of candidates, which is a list of all positions belonging to a subevent of the
|
||||
# maximum cardinality, where the cardinality of a subevent is defined as the number of tickets
|
||||
# for that subevent that are not yet part of any group
|
||||
candidates = []
|
||||
cardinality = None
|
||||
for se, l in subevent_to_idx.items():
|
||||
l = [ll for ll in l if ll not in current_group]
|
||||
if cardinality and len(l) != cardinality:
|
||||
continue
|
||||
if se not in {positions[idx][1] for idx in current_group}:
|
||||
candidates += l
|
||||
cardinality = len(l)
|
||||
|
||||
if not candidates:
|
||||
break
|
||||
|
||||
# Sort the list by prices, then pick one. For "buy 2 get 1 free" we apply a "pick 1 from the start
|
||||
# and 2 from the end" scheme to optimize price distribution among groups
|
||||
candidates = sorted(candidates, key=lambda idx: positions[idx][2])
|
||||
if len(current_group) < (self.benefit_only_apply_to_cheapest_n_matches or 0):
|
||||
candidate = candidates[0]
|
||||
else:
|
||||
candidate = candidates[-1]
|
||||
|
||||
current_group.append(candidate)
|
||||
|
||||
# Only add full groups to the list of groups
|
||||
if len(current_group) >= max(self.condition_min_count, 1):
|
||||
candidate_groups.append(current_group)
|
||||
for c in current_group:
|
||||
subevent_to_idx[positions[c][1]].remove(c)
|
||||
current_group = []
|
||||
|
||||
# Distribute "leftovers"
|
||||
for se in subevent_order:
|
||||
if subevent_to_idx[se]:
|
||||
for group in candidate_groups:
|
||||
if se not in {positions[idx][1] for idx in group}:
|
||||
group.append(subevent_to_idx[se].pop())
|
||||
if not subevent_to_idx[se]:
|
||||
break
|
||||
|
||||
for g in candidate_groups:
|
||||
self._apply_min_count(positions, g, result)
|
||||
return result
|
||||
@@ -700,8 +700,8 @@ class Event(EventMixin, LoggedModel):
|
||||
|
||||
from ..signals import event_copy_data
|
||||
from . import (
|
||||
Discount, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue,
|
||||
Question, Quota,
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, Question,
|
||||
Quota,
|
||||
)
|
||||
|
||||
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
|
||||
@@ -723,7 +723,7 @@ class Event(EventMixin, LoggedModel):
|
||||
tax_map[t.pk] = t
|
||||
t.pk = None
|
||||
t.event = self
|
||||
t.save(force_insert=True)
|
||||
t.save()
|
||||
t.log_action('pretix.object.cloned')
|
||||
|
||||
category_map = {}
|
||||
@@ -731,7 +731,7 @@ class Event(EventMixin, LoggedModel):
|
||||
category_map[c.pk] = c
|
||||
c.pk = None
|
||||
c.event = self
|
||||
c.save(force_insert=True)
|
||||
c.save()
|
||||
c.log_action('pretix.object.cloned')
|
||||
|
||||
item_meta_properties_map = {}
|
||||
@@ -739,7 +739,7 @@ class Event(EventMixin, LoggedModel):
|
||||
item_meta_properties_map[imp.pk] = imp
|
||||
imp.pk = None
|
||||
imp.event = self
|
||||
imp.save(force_insert=True)
|
||||
imp.save()
|
||||
imp.log_action('pretix.object.cloned')
|
||||
|
||||
item_map = {}
|
||||
@@ -760,7 +760,7 @@ class Event(EventMixin, LoggedModel):
|
||||
if i.grant_membership_type and other.organizer_id != self.organizer_id:
|
||||
i.grant_membership_type = None
|
||||
|
||||
i.save() # no force_insert since i.picture.save could have already inserted
|
||||
i.save()
|
||||
i.log_action('pretix.object.cloned')
|
||||
|
||||
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||
@@ -770,19 +770,19 @@ class Event(EventMixin, LoggedModel):
|
||||
variation_map[v.pk] = v
|
||||
v.pk = None
|
||||
v.item = i
|
||||
v.save(force_insert=True)
|
||||
v.save()
|
||||
|
||||
for imv in ItemMetaValue.objects.filter(item__event=other).prefetch_related('item', 'property'):
|
||||
imv.pk = None
|
||||
imv.property = item_meta_properties_map[imv.property.pk]
|
||||
imv.item = item_map[imv.item.pk]
|
||||
imv.save(force_insert=True)
|
||||
imv.save()
|
||||
|
||||
for ia in ItemAddOn.objects.filter(base_item__event=other).prefetch_related('base_item', 'addon_category'):
|
||||
ia.pk = None
|
||||
ia.base_item = item_map[ia.base_item.pk]
|
||||
ia.addon_category = category_map[ia.addon_category.pk]
|
||||
ia.save(force_insert=True)
|
||||
ia.save()
|
||||
|
||||
for ia in ItemBundle.objects.filter(base_item__event=other).prefetch_related('base_item', 'bundled_item', 'bundled_variation'):
|
||||
ia.pk = None
|
||||
@@ -790,7 +790,7 @@ class Event(EventMixin, LoggedModel):
|
||||
ia.bundled_item = item_map[ia.bundled_item.pk]
|
||||
if ia.bundled_variation:
|
||||
ia.bundled_variation = variation_map[ia.bundled_variation.pk]
|
||||
ia.save(force_insert=True)
|
||||
ia.save()
|
||||
|
||||
quota_map = {}
|
||||
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):
|
||||
@@ -801,7 +801,7 @@ class Event(EventMixin, LoggedModel):
|
||||
q.pk = None
|
||||
q.event = self
|
||||
q.closed = False
|
||||
q.save(force_insert=True)
|
||||
q.save()
|
||||
q.log_action('pretix.object.cloned')
|
||||
for i in items:
|
||||
if i.pk in item_map:
|
||||
@@ -810,16 +810,6 @@ class Event(EventMixin, LoggedModel):
|
||||
q.variations.add(variation_map[v.pk])
|
||||
self.items.filter(hidden_if_available_id=oldid).update(hidden_if_available=q)
|
||||
|
||||
for d in Discount.objects.filter(event=other).prefetch_related('condition_limit_products'):
|
||||
items = list(d.condition_limit_products.all())
|
||||
d.pk = None
|
||||
d.event = self
|
||||
d.save(force_insert=True)
|
||||
d.log_action('pretix.object.cloned')
|
||||
for i in items:
|
||||
if i.pk in item_map:
|
||||
d.condition_limit_products.add(item_map[i.pk])
|
||||
|
||||
question_map = {}
|
||||
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):
|
||||
items = list(q.items.all())
|
||||
@@ -827,7 +817,7 @@ class Event(EventMixin, LoggedModel):
|
||||
question_map[q.pk] = q
|
||||
q.pk = None
|
||||
q.event = self
|
||||
q.save(force_insert=True)
|
||||
q.save()
|
||||
q.log_action('pretix.object.cloned')
|
||||
|
||||
for i in items:
|
||||
@@ -835,7 +825,7 @@ class Event(EventMixin, LoggedModel):
|
||||
for o in opts:
|
||||
o.pk = None
|
||||
o.question = q
|
||||
o.save(force_insert=True)
|
||||
o.save()
|
||||
|
||||
for q in self.questions.filter(dependency_question__isnull=False):
|
||||
q.dependency_question = question_map[q.dependency_question_id]
|
||||
@@ -845,10 +835,10 @@ class Event(EventMixin, LoggedModel):
|
||||
if isinstance(rules, dict):
|
||||
for k, v in rules.items():
|
||||
if k == 'lookup':
|
||||
if rules[k][0] == 'product':
|
||||
rules[k][1] = str(item_map.get(int(v[1]), 0).pk) if int(v[1]) in item_map else "0"
|
||||
elif rules[k][0] == 'variation':
|
||||
rules[k][1] = str(variation_map.get(int(v[1]), 0).pk) if int(v[1]) in variation_map else "0"
|
||||
if v[0] == 'product':
|
||||
v[1] = str(item_map.get(int(v[1]), 0).pk) if int(v[1]) in item_map else "0"
|
||||
elif v[0] == 'variation':
|
||||
v[1] = str(variation_map.get(int(v[1]), 0).pk) if int(v[1]) in variation_map else "0"
|
||||
else:
|
||||
_walk_rules(v)
|
||||
elif isinstance(rules, list):
|
||||
@@ -864,7 +854,7 @@ class Event(EventMixin, LoggedModel):
|
||||
rules = cl.rules
|
||||
_walk_rules(rules)
|
||||
cl.rules = rules
|
||||
cl.save(force_insert=True)
|
||||
cl.save()
|
||||
cl.log_action('pretix.object.cloned')
|
||||
for i in items:
|
||||
cl.limit_products.add(item_map[i.pk])
|
||||
@@ -873,25 +863,21 @@ class Event(EventMixin, LoggedModel):
|
||||
if other.seating_plan.organizer_id == self.organizer_id:
|
||||
self.seating_plan = other.seating_plan
|
||||
else:
|
||||
sp = other.seating_plan
|
||||
sp.pk = None
|
||||
sp.organizer = self.organizer
|
||||
sp.save(force_insert=True)
|
||||
self.seating_plan = sp
|
||||
self.organizer.seating_plans.create(name=other.seating_plan.name, layout=other.seating_plan.layout)
|
||||
self.save()
|
||||
|
||||
for m in other.seat_category_mappings.filter(subevent__isnull=True):
|
||||
m.pk = None
|
||||
m.event = self
|
||||
m.product = item_map[m.product_id]
|
||||
m.save(force_insert=True)
|
||||
m.save()
|
||||
|
||||
for s in other.seats.filter(subevent__isnull=True):
|
||||
s.pk = None
|
||||
s.event = self
|
||||
if s.product_id:
|
||||
s.product = item_map[s.product_id]
|
||||
s.save(force_insert=True)
|
||||
s.save()
|
||||
|
||||
has_custom_style = other.settings.presale_css_file or other.settings.presale_widget_css_file
|
||||
skip_settings = (
|
||||
|
||||
@@ -1243,13 +1243,7 @@ class Question(LoggedModel):
|
||||
max_length=190,
|
||||
verbose_name=_("Internal identifier"),
|
||||
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
|
||||
'not input one, we will generate one automatically.'),
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex=r"^[a-zA-Z0-9.\-_]+$",
|
||||
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores."),
|
||||
),
|
||||
],
|
||||
'not input one, we will generate one automatically.')
|
||||
)
|
||||
help_text = I18nTextField(
|
||||
verbose_name=_("Help text"),
|
||||
@@ -1467,17 +1461,7 @@ class Question(LoggedModel):
|
||||
|
||||
class QuestionOption(models.Model):
|
||||
question = models.ForeignKey('Question', related_name='options', on_delete=models.CASCADE)
|
||||
identifier = models.CharField(
|
||||
max_length=190,
|
||||
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
|
||||
'not input one, we will generate one automatically.'),
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex=r"^[a-zA-Z0-9.\-_]+$",
|
||||
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores."),
|
||||
),
|
||||
],
|
||||
)
|
||||
identifier = models.CharField(max_length=190)
|
||||
answer = I18nCharField(verbose_name=_('Answer'))
|
||||
position = models.IntegerField(default=0)
|
||||
|
||||
|
||||
@@ -138,8 +138,8 @@ class LogEntry(models.Model):
|
||||
@cached_property
|
||||
def display_object(self):
|
||||
from . import (
|
||||
Discount, Event, Item, ItemCategory, Order, Question, Quota,
|
||||
SubEvent, TaxRule, Voucher,
|
||||
Event, Item, ItemCategory, Order, Question, Quota, SubEvent,
|
||||
TaxRule, Voucher,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -202,16 +202,6 @@ class LogEntry(models.Model):
|
||||
}),
|
||||
'val': escape(co.name),
|
||||
}
|
||||
elif isinstance(co, Discount):
|
||||
a_text = _('Discount {val}')
|
||||
a_map = {
|
||||
'href': reverse('control:event.items.discounts.edit', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
'discount': co.id
|
||||
}),
|
||||
'val': escape(co.internal_name),
|
||||
}
|
||||
elif isinstance(co, ItemCategory):
|
||||
a_text = _('Category {val}')
|
||||
a_map = {
|
||||
|
||||
@@ -906,10 +906,10 @@ class Order(LockModel, LoggedModel):
|
||||
if force:
|
||||
continue
|
||||
|
||||
if op.voucher and op.voucher.budget is not None and op.voucher_budget_use:
|
||||
if op.voucher and op.voucher.budget is not None and op.price_before_voucher is not None:
|
||||
if op.voucher not in v_budget:
|
||||
v_budget[op.voucher] = op.voucher.budget - op.voucher.budget_used()
|
||||
disc = op.voucher_budget_use
|
||||
disc = op.price_before_voucher - op.price
|
||||
if disc > v_budget[op.voucher]:
|
||||
raise Quota.QuotaExceededException(error_messages['voucher_budget'].format(
|
||||
voucher=op.voucher.code
|
||||
@@ -1275,6 +1275,9 @@ class AbstractPosition(models.Model):
|
||||
verbose_name=_("Variation"),
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
price_before_voucher = models.DecimalField(
|
||||
decimal_places=2, max_digits=10, null=True,
|
||||
)
|
||||
price = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
verbose_name=_("Price")
|
||||
@@ -1311,10 +1314,6 @@ class AbstractPosition(models.Model):
|
||||
)
|
||||
is_bundled = models.BooleanField(default=False)
|
||||
|
||||
discount = models.ForeignKey(
|
||||
'Discount', null=True, blank=True, on_delete=models.RESTRICT
|
||||
)
|
||||
|
||||
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
|
||||
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
|
||||
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True, null=True)
|
||||
@@ -2161,9 +2160,6 @@ class OrderPosition(AbstractPosition):
|
||||
related_name='all_positions',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
voucher_budget_use = models.DecimalField(
|
||||
max_digits=10, decimal_places=2, null=True, blank=True,
|
||||
)
|
||||
tax_rate = models.DecimalField(
|
||||
max_digits=7, decimal_places=2,
|
||||
verbose_name=_('Tax rate')
|
||||
@@ -2236,8 +2232,6 @@ class OrderPosition(AbstractPosition):
|
||||
else:
|
||||
setattr(op, f.name, getattr(cartpos, f.name))
|
||||
op._calculate_tax()
|
||||
if cartpos.voucher:
|
||||
op.voucher_budget_use = cartpos.listed_price - cartpos.price_after_voucher
|
||||
op.positionid = i + 1
|
||||
op.save()
|
||||
ops.append(op)
|
||||
@@ -2586,25 +2580,12 @@ class CartPosition(AbstractPosition):
|
||||
verbose_name=_("Expiration date"),
|
||||
db_index=True
|
||||
)
|
||||
|
||||
tax_rate = models.DecimalField(
|
||||
max_digits=7, decimal_places=2, default=Decimal('0.00'),
|
||||
verbose_name=_('Tax rate')
|
||||
includes_tax = models.BooleanField(
|
||||
default=True
|
||||
)
|
||||
listed_price = models.DecimalField(
|
||||
decimal_places=2, max_digits=10, null=True,
|
||||
)
|
||||
price_after_voucher = models.DecimalField(
|
||||
decimal_places=2, max_digits=10, null=True,
|
||||
)
|
||||
custom_price_input = models.DecimalField(
|
||||
decimal_places=2, max_digits=10, null=True,
|
||||
)
|
||||
custom_price_input_is_net = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
line_price_gross = models.DecimalField(
|
||||
decimal_places=2, max_digits=10, null=True,
|
||||
override_tax_rate = models.DecimalField(
|
||||
max_digits=10, decimal_places=2,
|
||||
null=True, blank=True
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
@@ -2618,66 +2599,21 @@ class CartPosition(AbstractPosition):
|
||||
self.item.id, self.variation.id if self.variation else 0, self.cart_id
|
||||
)
|
||||
|
||||
@property
|
||||
def tax_rate(self):
|
||||
if self.includes_tax:
|
||||
if self.override_tax_rate is not None:
|
||||
return self.override_tax_rate
|
||||
return self.item.tax(self.price, base_price_is='gross').rate
|
||||
else:
|
||||
return Decimal('0.00')
|
||||
|
||||
@property
|
||||
def tax_value(self):
|
||||
net = round_decimal(self.price - (self.price * (1 - 100 / (100 + self.tax_rate))),
|
||||
self.event.currency)
|
||||
return self.price - net
|
||||
|
||||
def update_listed_price_and_voucher(self, voucher_only=False, max_discount=None):
|
||||
from pretix.base.services.pricing import (
|
||||
get_listed_price, is_included_for_free,
|
||||
)
|
||||
|
||||
if voucher_only:
|
||||
listed_price = self.listed_price
|
||||
if self.includes_tax:
|
||||
return self.item.tax(self.price, override_tax_rate=self.override_tax_rate, base_price_is='gross').tax
|
||||
else:
|
||||
if self.addon_to_id and is_included_for_free(self.item, self.addon_to):
|
||||
listed_price = Decimal('0.00')
|
||||
else:
|
||||
listed_price = get_listed_price(self.item, self.variation, self.subevent)
|
||||
|
||||
if self.voucher:
|
||||
price_after_voucher = self.voucher.calculate_price(listed_price, max_discount)
|
||||
else:
|
||||
price_after_voucher = listed_price
|
||||
|
||||
if self.is_bundled:
|
||||
bundle = self.addon_to.item.bundles.filter(bundled_item=self.item, bundled_variation=self.variation).first()
|
||||
if bundle:
|
||||
listed_price = bundle.designated_price
|
||||
price_after_voucher = bundle.designated_price
|
||||
|
||||
if listed_price != self.listed_price or price_after_voucher != self.price_after_voucher:
|
||||
self.listed_price = listed_price
|
||||
self.price_after_voucher = price_after_voucher
|
||||
self.save(update_fields=['listed_price', 'price_after_voucher'])
|
||||
|
||||
def migrate_free_price_if_necessary(self):
|
||||
# Migrate from pre-discounts position
|
||||
if self.item.free_price and self.custom_price_input is None:
|
||||
custom_price = self.price
|
||||
if custom_price > 100000000:
|
||||
raise ValueError('price_too_high')
|
||||
self.custom_price_input = custom_price
|
||||
self.custom_price_input_is_net = not False
|
||||
self.save(update_fields=['custom_price_input', 'custom_price_input_is_net'])
|
||||
|
||||
def update_line_price(self, invoice_address, bundled_positions):
|
||||
from pretix.base.services.pricing import get_line_price
|
||||
|
||||
line_price = get_line_price(
|
||||
price_after_voucher=self.price_after_voucher,
|
||||
custom_price_input=self.custom_price_input,
|
||||
custom_price_input_is_net=self.custom_price_input_is_net,
|
||||
tax_rule=self.item.tax_rule,
|
||||
invoice_address=invoice_address,
|
||||
bundled_sum=sum([b.price_after_voucher for b in bundled_positions]),
|
||||
)
|
||||
if line_price.gross != self.line_price_gross or line_price.rate != self.tax_rate:
|
||||
self.line_price_gross = line_price.gross
|
||||
self.tax_rate = line_price.rate
|
||||
self.save(update_fields=['line_price_gross', 'tax_rate'])
|
||||
return Decimal('0.00')
|
||||
|
||||
|
||||
class InvoiceAddress(models.Model):
|
||||
|
||||
@@ -39,7 +39,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import connection, models
|
||||
from django.db.models import OuterRef, Q, Subquery, Sum
|
||||
from django.db.models import F, OuterRef, Q, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
@@ -530,8 +530,6 @@ class Voucher(LoggedModel):
|
||||
original price will be returned.
|
||||
"""
|
||||
if self.value is not None:
|
||||
if not isinstance(self.value, Decimal):
|
||||
self.value = Decimal(self.value)
|
||||
if self.price_mode == 'set':
|
||||
p = self.value
|
||||
elif self.price_mode == 'subtract':
|
||||
@@ -571,21 +569,21 @@ class Voucher(LoggedModel):
|
||||
def annotate_budget_used_orders(cls, qs):
|
||||
opq = OrderPosition.objects.filter(
|
||||
voucher_id=OuterRef('pk'),
|
||||
voucher_budget_use__isnull=False,
|
||||
price_before_voucher__isnull=False,
|
||||
order__status__in=[
|
||||
Order.STATUS_PAID,
|
||||
Order.STATUS_PENDING
|
||||
]
|
||||
).order_by().values('voucher_id').annotate(s=Sum('voucher_budget_use')).values('s')
|
||||
).order_by().values('voucher_id').annotate(s=Sum(F('price_before_voucher') - F('price'))).values('s')
|
||||
return qs.annotate(budget_used_orders=Coalesce(Subquery(opq, output_field=models.DecimalField(max_digits=10, decimal_places=2)), Decimal('0.00')))
|
||||
|
||||
def budget_used(self):
|
||||
ops = OrderPosition.objects.filter(
|
||||
voucher=self,
|
||||
voucher_budget_use__isnull=False,
|
||||
price_before_voucher__isnull=False,
|
||||
order__status__in=[
|
||||
Order.STATUS_PAID,
|
||||
Order.STATUS_PENDING
|
||||
]
|
||||
).aggregate(s=Sum('voucher_budget_use'))['s'] or Decimal('0.00')
|
||||
).aggregate(s=Sum(F('price_before_voucher') - F('price')))['s'] or Decimal('0.00')
|
||||
return ops
|
||||
|
||||
@@ -55,7 +55,7 @@ from django.utils.formats import date_format
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from PyPDF2 import PdfFileReader
|
||||
from pytz import timezone
|
||||
@@ -521,19 +521,12 @@ def variables_from_questions(sender, *args, **kwargs):
|
||||
|
||||
def _get_attendee_name_part(key, op, order, ev):
|
||||
if isinstance(key, tuple):
|
||||
parts = [_get_attendee_name_part(c[0], op, order, ev) for c in key if not (c[0] == 'salutation' and op.attendee_name_parts.get(c[0], '') == "Mx")]
|
||||
return ' '.join(p for p in parts if p)
|
||||
value = op.attendee_name_parts.get(key, '')
|
||||
if key == 'salutation':
|
||||
return pgettext('person_name_salutation', value)
|
||||
return value
|
||||
return ' '.join(p for p in [_get_attendee_name_part(c[0], op, order, ev) for c in key] if p)
|
||||
return op.attendee_name_parts.get(key, '')
|
||||
|
||||
|
||||
def _get_ia_name_part(key, op, order, ev):
|
||||
value = order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
|
||||
if key == 'salutation' and value:
|
||||
return pgettext('person_name_salutation', value)
|
||||
return value
|
||||
return order.invoice_address.name_parts.get(key, '') if getattr(order, 'invoice_address', None) else ''
|
||||
|
||||
|
||||
def get_images(event):
|
||||
@@ -549,14 +542,6 @@ def get_variables(event):
|
||||
v = copy.copy(DEFAULT_VARIABLES)
|
||||
|
||||
scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
||||
|
||||
concatenation_for_salutation = scheme.get("concatenation_for_salutation", scheme["concatenation"])
|
||||
v['attendee_name_for_salutation'] = {
|
||||
'label': _("Attendee name for salutation"),
|
||||
'editor_sample': _("Mr Doe"),
|
||||
'evaluate': lambda op, order, ev: concatenation_for_salutation(op.attendee_name_parts or {})
|
||||
}
|
||||
|
||||
for key, label, weight in scheme['fields']:
|
||||
v['attendee_name_%s' % key] = {
|
||||
'label': _("Attendee name: {part}").format(part=label),
|
||||
@@ -574,12 +559,6 @@ def get_variables(event):
|
||||
v['invoice_name']['editor_sample'] = scheme['concatenation'](scheme['sample'])
|
||||
v['attendee_name']['editor_sample'] = scheme['concatenation'](scheme['sample'])
|
||||
|
||||
v['invoice_name_for_salutation'] = {
|
||||
'label': _("Invoice address name for salutation"),
|
||||
'editor_sample': _("Mr Doe"),
|
||||
'evaluate': lambda op, order, ev: concatenation_for_salutation(order.invoice_address.name_parts if getattr(order, 'invoice_address', None) else {})
|
||||
}
|
||||
|
||||
for key, label, weight in scheme['fields']:
|
||||
v['invoice_name_%s' % key] = {
|
||||
'label': _("Invoice address name: {part}").format(part=label),
|
||||
@@ -734,6 +713,7 @@ class Renderer:
|
||||
text = o['text']
|
||||
|
||||
def replace(x):
|
||||
print(x.group(1))
|
||||
if x.group(1).startswith('itemmeta:'):
|
||||
return op.item.meta_data.get(x.group(1)[9:]) or ''
|
||||
elif x.group(1).startswith('meta:'):
|
||||
|
||||
@@ -54,14 +54,11 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.orders import OrderFee
|
||||
from pretix.base.models.tax import TaxRule
|
||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.services.checkin import _save_answers
|
||||
from pretix.base.services.locking import LockTimeoutException, NoLockManager
|
||||
from pretix.base.services.pricing import (
|
||||
apply_discounts, get_line_price, get_listed_price, get_price,
|
||||
is_included_for_free,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES, LazyI18nStringList
|
||||
@@ -148,15 +145,13 @@ error_messages = {
|
||||
|
||||
|
||||
class CartManager:
|
||||
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'voucher', 'quotas',
|
||||
'addon_to', 'subevent', 'bundled', 'seat', 'listed_price',
|
||||
'price_after_voucher', 'custom_price_input',
|
||||
'custom_price_input_is_net'))
|
||||
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'price', 'voucher', 'quotas',
|
||||
'addon_to', 'subevent', 'includes_tax', 'bundled', 'seat',
|
||||
'price_before_voucher'))
|
||||
RemoveOperation = namedtuple('RemoveOperation', ('position',))
|
||||
VoucherOperation = namedtuple('VoucherOperation', ('position', 'voucher', 'price_after_voucher'))
|
||||
ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'voucher',
|
||||
'quotas', 'subevent', 'seat', 'listed_price',
|
||||
'price_after_voucher'))
|
||||
VoucherOperation = namedtuple('VoucherOperation', ('position', 'voucher', 'price'))
|
||||
ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'price', 'voucher',
|
||||
'quotas', 'subevent', 'seat', 'price_before_voucher'))
|
||||
order = {
|
||||
RemoveOperation: 10,
|
||||
VoucherOperation: 15,
|
||||
@@ -183,8 +178,8 @@ class CartManager:
|
||||
|
||||
@property
|
||||
def positions(self):
|
||||
return self.event.cartposition_set.filter(
|
||||
Q(cart_id=self.cart_id)
|
||||
return CartPosition.objects.filter(
|
||||
Q(cart_id=self.cart_id) & Q(event=self.event)
|
||||
).select_related('item', 'subevent')
|
||||
|
||||
def _is_seated(self, item, subevent):
|
||||
@@ -395,6 +390,7 @@ class CartManager:
|
||||
'addons'
|
||||
).order_by('-is_bundled')
|
||||
err = None
|
||||
changed_prices = {}
|
||||
for cp in expired:
|
||||
removed_positions = {op.position.pk for op in self._operations if isinstance(op, self.RemoveOperation)}
|
||||
if cp.pk in removed_positions or (cp.addon_to_id and cp.addon_to_id in removed_positions):
|
||||
@@ -405,16 +401,40 @@ class CartManager:
|
||||
if cp.is_bundled:
|
||||
bundle = cp.addon_to.item.bundles.filter(bundled_item=cp.item, bundled_variation=cp.variation).first()
|
||||
if bundle:
|
||||
listed_price = bundle.designated_price or 0
|
||||
price = bundle.designated_price or 0
|
||||
else:
|
||||
listed_price = cp.price
|
||||
price_after_voucher = listed_price
|
||||
price = cp.price
|
||||
|
||||
changed_prices[cp.pk] = price
|
||||
|
||||
if not cp.includes_tax:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, price, cp.subevent,
|
||||
force_custom_price=True, cp_is_net=False)
|
||||
price = TaxedPrice(net=price.net, gross=price.net, rate=0, tax=0, name='')
|
||||
else:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, price, cp.subevent,
|
||||
force_custom_price=True)
|
||||
pbv = TAXED_ZERO
|
||||
else:
|
||||
listed_price = get_listed_price(cp.item, cp.variation, cp.subevent)
|
||||
if cp.voucher:
|
||||
price_after_voucher = cp.voucher.calculate_price(listed_price)
|
||||
bundled_sum = Decimal('0.00')
|
||||
if not cp.addon_to_id:
|
||||
for bundledp in cp.addons.all():
|
||||
if bundledp.is_bundled:
|
||||
bundledprice = changed_prices.get(bundledp.pk, bundledp.price)
|
||||
bundled_sum += bundledprice
|
||||
|
||||
if not cp.includes_tax:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
|
||||
cp_is_net=True, bundled_sum=bundled_sum)
|
||||
price = TaxedPrice(net=price.net, gross=price.net, rate=Decimal('0'), tax=Decimal('0'), name='')
|
||||
pbv = self._get_price(cp.item, cp.variation, None, cp.price, cp.subevent,
|
||||
cp_is_net=True, bundled_sum=bundled_sum)
|
||||
pbv = TaxedPrice(net=pbv.net, gross=pbv.net, rate=Decimal('0'), tax=Decimal('0'), name='')
|
||||
else:
|
||||
price_after_voucher = listed_price
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
|
||||
bundled_sum=bundled_sum)
|
||||
pbv = self._get_price(cp.item, cp.variation, None, cp.price, cp.subevent,
|
||||
bundled_sum=bundled_sum)
|
||||
|
||||
quotas = list(cp.quotas)
|
||||
if not quotas:
|
||||
@@ -430,8 +450,7 @@ class CartManager:
|
||||
|
||||
op = self.ExtendOperation(
|
||||
position=cp, item=cp.item, variation=cp.variation, voucher=cp.voucher, count=1,
|
||||
quotas=quotas, subevent=cp.subevent, seat=cp.seat, listed_price=listed_price,
|
||||
price_after_voucher=price_after_voucher,
|
||||
price=price, quotas=quotas, subevent=cp.subevent, seat=cp.seat, price_before_voucher=pbv
|
||||
)
|
||||
self._check_item_constraints(op)
|
||||
|
||||
@@ -447,10 +466,7 @@ class CartManager:
|
||||
try:
|
||||
voucher = self.event.vouchers.get(code__iexact=voucher_code.strip())
|
||||
except Voucher.DoesNotExist:
|
||||
if self.event.organizer.accepted_gift_cards.filter(secret__iexact=voucher_code).exists():
|
||||
raise CartError(error_messages['gift_card'])
|
||||
else:
|
||||
raise CartError(error_messages['voucher_invalid'])
|
||||
raise CartError(error_messages['voucher_invalid'])
|
||||
voucher_use_diff = Counter()
|
||||
ops = []
|
||||
|
||||
@@ -473,22 +489,26 @@ class CartManager:
|
||||
if p.is_bundled:
|
||||
continue
|
||||
|
||||
if p.listed_price is None:
|
||||
if p.addon_to_id and is_included_for_free(p.item, p.addon_to):
|
||||
listed_price = Decimal('0.00')
|
||||
else:
|
||||
listed_price = get_listed_price(p.item, p.variation, p.subevent)
|
||||
else:
|
||||
listed_price = p.listed_price
|
||||
price_after_voucher = voucher.calculate_price(listed_price)
|
||||
bundled_sum = Decimal('0.00')
|
||||
if not p.addon_to_id:
|
||||
for bundledp in p.addons.all():
|
||||
if bundledp.is_bundled:
|
||||
bundledprice = bundledp.price
|
||||
bundled_sum += bundledprice
|
||||
|
||||
price = self._get_price(p.item, p.variation, voucher, None, p.subevent, bundled_sum=bundled_sum)
|
||||
"""
|
||||
if price.gross > p.price:
|
||||
continue
|
||||
"""
|
||||
|
||||
voucher_use_diff[voucher] += 1
|
||||
ops.append((listed_price - price_after_voucher, self.VoucherOperation(p, voucher, price_after_voucher)))
|
||||
ops.append((p.price - price.gross, self.VoucherOperation(p, voucher, price)))
|
||||
|
||||
# If there are not enough voucher usages left for the full cart, let's apply them in the order that benefits
|
||||
# the user the most.
|
||||
ops.sort(key=lambda k: k[0], reverse=True)
|
||||
self._operations += [k[1] for k in ops]
|
||||
self._operations += [k[1] for k in ops]\
|
||||
|
||||
if not voucher_use_diff:
|
||||
raise CartError(error_messages['voucher_no_match'])
|
||||
@@ -555,6 +575,7 @@ class CartManager:
|
||||
|
||||
# Fetch bundled items
|
||||
bundled = []
|
||||
bundled_sum = Decimal('0.00')
|
||||
db_bundles = list(item.bundles.all())
|
||||
self._update_items_cache([b.bundled_item_id for b in db_bundles], [b.bundled_variation_id for b in db_bundles])
|
||||
for bundle in db_bundles:
|
||||
@@ -574,49 +595,28 @@ class CartManager:
|
||||
else:
|
||||
bundle_quotas = []
|
||||
|
||||
if bundle.designated_price:
|
||||
bprice = self._get_price(bitem, bvar, None, bundle.designated_price, subevent, force_custom_price=True,
|
||||
cp_is_net=False)
|
||||
else:
|
||||
bprice = TAXED_ZERO
|
||||
bundled_sum += bundle.designated_price * bundle.count
|
||||
|
||||
bop = self.AddOperation(
|
||||
count=bundle.count,
|
||||
item=bitem,
|
||||
variation=bvar,
|
||||
voucher=None,
|
||||
quotas=bundle_quotas,
|
||||
addon_to='FAKE',
|
||||
subevent=subevent,
|
||||
bundled=[],
|
||||
seat=None,
|
||||
listed_price=bundle.designated_price,
|
||||
price_after_voucher=bundle.designated_price,
|
||||
custom_price_input=None,
|
||||
custom_price_input_is_net=False,
|
||||
count=bundle.count, item=bitem, variation=bvar, price=bprice,
|
||||
voucher=None, quotas=bundle_quotas, addon_to='FAKE', subevent=subevent,
|
||||
includes_tax=bool(bprice.rate), bundled=[], seat=None, price_before_voucher=bprice,
|
||||
)
|
||||
self._check_item_constraints(bop, operations)
|
||||
bundled.append(bop)
|
||||
|
||||
listed_price = get_listed_price(item, variation, subevent)
|
||||
if voucher:
|
||||
price_after_voucher = voucher.calculate_price(listed_price)
|
||||
else:
|
||||
price_after_voucher = listed_price
|
||||
custom_price = None
|
||||
if item.free_price and i.get('price'):
|
||||
custom_price = Decimal(str(i.get('price')).replace(",", "."))
|
||||
if custom_price > 100000000:
|
||||
raise ValueError('price_too_high')
|
||||
price = self._get_price(item, variation, voucher, i.get('price'), subevent, bundled_sum=bundled_sum)
|
||||
pbv = self._get_price(item, variation, None, i.get('price'), subevent, bundled_sum=bundled_sum)
|
||||
|
||||
op = self.AddOperation(
|
||||
count=i['count'],
|
||||
item=item,
|
||||
variation=variation,
|
||||
voucher=voucher,
|
||||
quotas=quotas,
|
||||
addon_to=False,
|
||||
subevent=subevent,
|
||||
bundled=bundled,
|
||||
seat=seat,
|
||||
listed_price=listed_price,
|
||||
price_after_voucher=price_after_voucher,
|
||||
custom_price_input=custom_price,
|
||||
custom_price_input_is_net=self.event.settings.display_net_prices,
|
||||
count=i['count'], item=item, variation=variation, price=price, voucher=voucher, quotas=quotas,
|
||||
addon_to=False, subevent=subevent, includes_tax=bool(price.rate), bundled=bundled, seat=seat,
|
||||
price_before_voucher=pbv
|
||||
)
|
||||
self._check_item_constraints(op, operations)
|
||||
operations.append(op)
|
||||
@@ -707,27 +707,16 @@ class CartManager:
|
||||
input_addons[cp.id][a['item'], a['variation']] = a.get('count', 1)
|
||||
selected_addons[cp.id, item.category_id][a['item'], a['variation']] = a.get('count', 1)
|
||||
|
||||
if is_included_for_free(item, cp):
|
||||
listed_price = Decimal('0.00')
|
||||
if price_included[cp.pk].get(item.category_id):
|
||||
price = TAXED_ZERO
|
||||
else:
|
||||
listed_price = get_listed_price(item, variation, cp.subevent)
|
||||
custom_price = None
|
||||
if item.free_price and a.get('price'):
|
||||
custom_price = Decimal(str(a.get('price')).replace(",", "."))
|
||||
if custom_price > 100000000:
|
||||
raise ValueError('price_too_high')
|
||||
price = self._get_price(item, variation, None, a.get('price'), cp.subevent)
|
||||
|
||||
# Fix positions with wrong price (TODO: happens out-of-cartmanager-transaction and therefore a little hacky)
|
||||
for ca in current_addons[cp][a['item'], a['variation']]:
|
||||
if ca.listed_price != listed_price:
|
||||
ca.listed_price = ca.listed_price
|
||||
ca.price_after_voucher = ca.price_after_voucher
|
||||
ca.save(update_fields=['listed_price', 'price_after_voucher'])
|
||||
if ca.custom_price_input != custom_price:
|
||||
ca.custom_price_input = custom_price
|
||||
ca.custom_price_input_is_net = self.event.settings.display_net_prices
|
||||
ca.price_after_voucher = ca.price_after_voucher
|
||||
ca.save(update_fields=['custom_price_input', 'custom_price_input'])
|
||||
if ca.price != price.gross:
|
||||
ca.price = price.gross
|
||||
ca.save(update_fields=['price'])
|
||||
|
||||
if a.get('count', 1) > len(current_addons[cp][a['item'], a['variation']]):
|
||||
# This add-on is new, add it to the cart
|
||||
@@ -736,18 +725,9 @@ class CartManager:
|
||||
|
||||
op = self.AddOperation(
|
||||
count=a.get('count', 1) - len(current_addons[cp][a['item'], a['variation']]),
|
||||
item=item,
|
||||
variation=variation,
|
||||
voucher=None,
|
||||
quotas=quotas,
|
||||
addon_to=cp,
|
||||
subevent=cp.subevent,
|
||||
bundled=[],
|
||||
seat=None,
|
||||
listed_price=listed_price,
|
||||
price_after_voucher=listed_price,
|
||||
custom_price_input=custom_price,
|
||||
custom_price_input_is_net=self.event.settings.display_net_prices,
|
||||
item=item, variation=variation, price=price, voucher=None, quotas=quotas,
|
||||
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=None,
|
||||
price_before_voucher=None
|
||||
)
|
||||
self._check_item_constraints(op, operations)
|
||||
operations.append(op)
|
||||
@@ -992,31 +972,13 @@ class CartManager:
|
||||
err = err or error_messages['seat_unavailable']
|
||||
|
||||
for k in range(available_count):
|
||||
line_price = get_line_price(
|
||||
price_after_voucher=op.price_after_voucher,
|
||||
custom_price_input=op.custom_price_input,
|
||||
custom_price_input_is_net=op.custom_price_input_is_net,
|
||||
tax_rule=op.item.tax_rule,
|
||||
invoice_address=self.invoice_address,
|
||||
bundled_sum=sum([pp.count * pp.price_after_voucher for pp in op.bundled]),
|
||||
)
|
||||
cp = CartPosition(
|
||||
event=self.event,
|
||||
item=op.item,
|
||||
variation=op.variation,
|
||||
expires=self._expiry,
|
||||
cart_id=self.cart_id,
|
||||
voucher=op.voucher,
|
||||
addon_to=op.addon_to if op.addon_to else None,
|
||||
subevent=op.subevent,
|
||||
seat=op.seat,
|
||||
listed_price=op.listed_price,
|
||||
price_after_voucher=op.price_after_voucher,
|
||||
custom_price_input=op.custom_price_input,
|
||||
custom_price_input_is_net=op.custom_price_input_is_net,
|
||||
line_price_gross=line_price.gross,
|
||||
tax_rate=line_price.tax,
|
||||
price=line_price.gross,
|
||||
event=self.event, item=op.item, variation=op.variation,
|
||||
price=op.price.gross, expires=self._expiry, cart_id=self.cart_id,
|
||||
voucher=op.voucher, addon_to=op.addon_to if op.addon_to else None,
|
||||
subevent=op.subevent, includes_tax=op.includes_tax, seat=op.seat,
|
||||
override_tax_rate=op.price.rate,
|
||||
price_before_voucher=op.price_before_voucher.gross if op.price_before_voucher is not None else None
|
||||
)
|
||||
if self.event.settings.attendee_names_asked:
|
||||
scheme = PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme)
|
||||
@@ -1045,26 +1007,12 @@ class CartManager:
|
||||
if op.bundled:
|
||||
cp.save() # Needs to be in the database already so we have a PK that we can reference
|
||||
for b in op.bundled:
|
||||
bline_price = (
|
||||
b.item.tax_rule or TaxRule(rate=Decimal('0.00'))
|
||||
).tax(b.listed_price, base_price_is='gross', invoice_address=self.invoice_address) # todo compare with previous behaviour
|
||||
for j in range(b.count):
|
||||
new_cart_positions.append(CartPosition(
|
||||
event=self.event,
|
||||
item=b.item,
|
||||
variation=b.variation,
|
||||
expires=self._expiry, cart_id=self.cart_id,
|
||||
voucher=None,
|
||||
addon_to=cp,
|
||||
subevent=b.subevent,
|
||||
listed_price=b.listed_price,
|
||||
price_after_voucher=b.price_after_voucher,
|
||||
custom_price_input=b.custom_price_input,
|
||||
custom_price_input_is_net=b.custom_price_input_is_net,
|
||||
line_price_gross=bline_price.gross,
|
||||
tax_rate=bline_price.tax,
|
||||
price=bline_price.gross,
|
||||
is_bundled=True
|
||||
event=self.event, item=b.item, variation=b.variation,
|
||||
price=b.price.gross, expires=self._expiry, cart_id=self.cart_id,
|
||||
voucher=None, addon_to=cp, override_tax_rate=b.price.rate,
|
||||
subevent=b.subevent, includes_tax=b.includes_tax, is_bundled=True
|
||||
))
|
||||
|
||||
new_cart_positions.append(cp)
|
||||
@@ -1076,11 +1024,11 @@ class CartManager:
|
||||
op.position.delete()
|
||||
elif available_count == 1:
|
||||
op.position.expires = self._expiry
|
||||
op.position.listed_price = op.listed_price
|
||||
op.position.price_after_voucher = op.price_after_voucher
|
||||
# op.position.price will be updated by recompute_final_prices_and_taxes()
|
||||
op.position.price = op.price.gross
|
||||
if op.price_before_voucher is not None:
|
||||
op.position.price_before_voucher = op.price_before_voucher.gross
|
||||
try:
|
||||
op.position.save(force_update=True, update_fields=['expires', 'listed_price', 'price_after_voucher'])
|
||||
op.position.save(force_update=True)
|
||||
except DatabaseError:
|
||||
# Best effort... The position might have been deleted in the meantime!
|
||||
pass
|
||||
@@ -1098,10 +1046,10 @@ class CartManager:
|
||||
# be expected
|
||||
continue
|
||||
|
||||
op.position.price_after_voucher = op.price_after_voucher
|
||||
op.position.price_before_voucher = op.position.price
|
||||
op.position.price = op.price.gross
|
||||
op.position.voucher = op.voucher
|
||||
# op.posiiton.price will be set in recompute_final_prices_and_taxes
|
||||
op.position.save(update_fields=['price_after_voucher', 'voucher'])
|
||||
op.position.save()
|
||||
vouchers_ok[op.voucher] -= 1
|
||||
|
||||
for p in new_cart_positions:
|
||||
@@ -1126,35 +1074,6 @@ class CartManager:
|
||||
|
||||
return False
|
||||
|
||||
def recompute_final_prices_and_taxes(self):
|
||||
positions = sorted(list(self.positions), key=lambda op: -(op.addon_to_id or 0))
|
||||
diff = Decimal('0.00')
|
||||
for cp in positions:
|
||||
if cp.listed_price is None:
|
||||
# migration from old system? also used in unit tests
|
||||
cp.update_listed_price_and_voucher()
|
||||
cp.migrate_free_price_if_necessary()
|
||||
|
||||
cp.update_line_price(self.invoice_address, [b for b in positions if b.addon_to_id == cp.pk and b.is_bundled])
|
||||
|
||||
discount_results = apply_discounts(
|
||||
self.event,
|
||||
self._sales_channel,
|
||||
[
|
||||
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
||||
for cp in positions
|
||||
]
|
||||
)
|
||||
|
||||
for cp, (new_price, discount) in zip(positions, discount_results):
|
||||
if cp.price != new_price or cp.discount_id != (discount.pk if discount else None):
|
||||
diff += new_price - cp.price
|
||||
cp.price = new_price
|
||||
cp.discount = discount
|
||||
cp.save(update_fields=['price', 'discount'])
|
||||
|
||||
return diff
|
||||
|
||||
def commit(self):
|
||||
self._check_presale_dates()
|
||||
self._check_max_cart_size()
|
||||
@@ -1172,11 +1091,33 @@ class CartManager:
|
||||
self.now_dt = now_dt
|
||||
self._extend_expiry_of_valid_existing_positions()
|
||||
err = self._perform_operations() or err
|
||||
self.recompute_final_prices_and_taxes()
|
||||
if err:
|
||||
raise CartError(err)
|
||||
|
||||
|
||||
def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress):
|
||||
positions = CartPosition.objects.filter(
|
||||
cart_id=cart_id, event=event
|
||||
).select_related('item', 'item__tax_rule')
|
||||
totaldiff = Decimal('0.00')
|
||||
for pos in positions:
|
||||
if not pos.item.tax_rule:
|
||||
continue
|
||||
rate = pos.item.tax_rule.tax_rate_for(invoice_address)
|
||||
|
||||
if pos.tax_rate != rate:
|
||||
if not pos.item.tax_rule.keep_gross_if_rate_changes:
|
||||
current_net = pos.price - pos.tax_value
|
||||
new_gross = pos.item.tax(current_net, base_price_is='net', invoice_address=invoice_address).gross
|
||||
totaldiff += new_gross - pos.price
|
||||
pos.price = new_gross
|
||||
pos.includes_tax = rate != Decimal('0.00')
|
||||
pos.override_tax_rate = rate
|
||||
pos.save(update_fields=['price', 'includes_tax', 'override_tax_rate'])
|
||||
|
||||
return totaldiff
|
||||
|
||||
|
||||
def get_fees(event, request, total, invoice_address, provider, positions):
|
||||
from pretix.presale.views.cart import cart_session
|
||||
|
||||
|
||||
@@ -796,7 +796,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
gate=device.gate if device else None,
|
||||
nonce=nonce,
|
||||
forced=force and (not entry_allowed or from_revoked_secret),
|
||||
force_sent=force,
|
||||
raw_barcode=raw_barcode,
|
||||
)
|
||||
op.order.log_action('pretix.event.checkin', data={
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from collections import Counter, defaultdict, namedtuple
|
||||
from datetime import datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
@@ -69,6 +68,7 @@ from pretix.base.models import (
|
||||
Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import ItemBundle
|
||||
from pretix.base.models.orders import (
|
||||
InvoiceAddress, OrderFee, OrderRefund, generate_secret,
|
||||
)
|
||||
@@ -86,9 +86,7 @@ from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.memberships import (
|
||||
create_membership, validate_memberships_in_order,
|
||||
)
|
||||
from pretix.base.services.pricing import (
|
||||
apply_discounts, get_listed_price, get_price,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
|
||||
from pretix.base.signals import (
|
||||
@@ -567,8 +565,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
_check_date(event, now_dt)
|
||||
|
||||
products_seen = Counter()
|
||||
q_avail = Counter()
|
||||
v_avail = Counter()
|
||||
changed_prices = {}
|
||||
v_budget = {}
|
||||
deleted_positions = set()
|
||||
seats_seen = set()
|
||||
@@ -585,8 +582,6 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
cp.delete()
|
||||
|
||||
sorted_positions = sorted(positions, key=lambda s: -int(s.is_bundled))
|
||||
|
||||
# Check availability
|
||||
for i, cp in enumerate(sorted_positions):
|
||||
if cp.pk in deleted_positions:
|
||||
continue
|
||||
@@ -606,17 +601,29 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
break
|
||||
|
||||
if cp.voucher:
|
||||
if cp.voucher not in v_avail:
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt)
|
||||
).exclude(cart_id=cp.cart_id)
|
||||
v_avail[cp.voucher] = cp.voucher.max_usages - cp.voucher.redeemed - redeemed_in_carts.count()
|
||||
v_avail[cp.voucher] -= 1
|
||||
if v_avail[cp.voucher] < 0:
|
||||
redeemed_in_carts = CartPosition.objects.filter(
|
||||
Q(voucher=cp.voucher) & Q(event=event) & Q(expires__gte=now_dt)
|
||||
).exclude(pk=cp.pk)
|
||||
v_avail = cp.voucher.max_usages - cp.voucher.redeemed - redeemed_in_carts.count()
|
||||
if v_avail < 1:
|
||||
err = err or error_messages['voucher_redeemed']
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.voucher.budget is not None:
|
||||
if cp.voucher not in v_budget:
|
||||
v_budget[cp.voucher] = cp.voucher.budget - cp.voucher.budget_used()
|
||||
disc = cp.price_before_voucher - cp.price
|
||||
if disc > v_budget[cp.voucher]:
|
||||
new_disc = max(0, v_budget[cp.voucher])
|
||||
cp.price = cp.price + (disc - new_disc)
|
||||
cp.save()
|
||||
err = err or error_messages['voucher_budget_used']
|
||||
v_budget[cp.voucher] -= new_disc
|
||||
continue
|
||||
else:
|
||||
v_budget[cp.voucher] -= disc
|
||||
|
||||
if cp.subevent and cp.subevent.presale_start and now_dt < cp.subevent.presale_start:
|
||||
err = err or error_messages['some_subevent_not_started']
|
||||
delete(cp)
|
||||
@@ -655,6 +662,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
cp.voucher is None or not cp.voucher.show_hidden_items or not cp.voucher.applies_to(cp.item, cp.variation)
|
||||
) and not cp.is_bundled:
|
||||
delete(cp)
|
||||
cp.delete()
|
||||
err = error_messages['voucher_required']
|
||||
break
|
||||
|
||||
@@ -663,14 +671,56 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
# time, since we absolutely can not overbook a seat.
|
||||
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel):
|
||||
err = err or error_messages['seat_unavailable']
|
||||
delete(cp)
|
||||
cp.delete()
|
||||
continue
|
||||
|
||||
if cp.expires >= now_dt and not cp.voucher:
|
||||
# Other checks are not necessary
|
||||
continue
|
||||
|
||||
if len(quotas) == 0:
|
||||
max_discount = None
|
||||
if cp.price_before_voucher is not None and cp.voucher in v_budget:
|
||||
current_discount = cp.price_before_voucher - cp.price
|
||||
max_discount = max(v_budget[cp.voucher] + current_discount, 0)
|
||||
|
||||
try:
|
||||
if cp.is_bundled:
|
||||
try:
|
||||
bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation)
|
||||
bprice = bundle.designated_price or 0
|
||||
except ItemBundle.DoesNotExist:
|
||||
bprice = cp.price
|
||||
except ItemBundle.MultipleObjectsReturned:
|
||||
raise OrderError("Invalid product configuration (duplicate bundle)")
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
|
||||
custom_price_is_tax_rate=cp.override_tax_rate,
|
||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||
pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
|
||||
custom_price_is_tax_rate=cp.override_tax_rate,
|
||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||
changed_prices[cp.pk] = bprice
|
||||
else:
|
||||
bundled_sum = Decimal('0.00')
|
||||
if not cp.addon_to_id:
|
||||
for bundledp in cp.addons.all():
|
||||
if bundledp.is_bundled:
|
||||
bundled_sum += changed_prices.get(bundledp.pk, bundledp.price)
|
||||
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
|
||||
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
|
||||
max_discount=max_discount, custom_price_is_tax_rate=cp.override_tax_rate)
|
||||
pbv = get_price(cp.item, cp.variation, None, cp.price, cp.subevent, custom_price_is_net=False,
|
||||
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
|
||||
max_discount=max_discount, custom_price_is_tax_rate=cp.override_tax_rate)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
err = err or error_messages['country_blocked']
|
||||
cp.delete()
|
||||
continue
|
||||
|
||||
if max_discount is not None:
|
||||
v_budget[cp.voucher] = v_budget[cp.voucher] + current_discount - (pbv.gross - price.gross)
|
||||
|
||||
if price is False or len(quotas) == 0:
|
||||
err = err or error_messages['unavailable']
|
||||
delete(cp)
|
||||
continue
|
||||
@@ -692,88 +742,42 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if pbv is not None and pbv.gross != price.gross:
|
||||
cp.price_before_voucher = pbv.gross
|
||||
else:
|
||||
cp.price_before_voucher = None
|
||||
|
||||
if price.gross != cp.price and not (cp.item.free_price and cp.price > price.gross):
|
||||
cp.price = price.gross
|
||||
cp.includes_tax = bool(price.rate)
|
||||
cp.save()
|
||||
err = err or error_messages['price_changed']
|
||||
continue
|
||||
|
||||
quota_ok = True
|
||||
|
||||
ignore_all_quotas = cp.expires >= now_dt or (
|
||||
cp.voucher and (
|
||||
cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)
|
||||
)
|
||||
)
|
||||
cp.voucher and (cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)))
|
||||
|
||||
if not ignore_all_quotas:
|
||||
for quota in quotas:
|
||||
if cp.voucher and cp.voucher.block_quota and cp.voucher.quota_id == quota.pk:
|
||||
continue
|
||||
if quota not in q_avail:
|
||||
avail = quota.availability(now_dt)
|
||||
q_avail[quota] = avail[1] if avail[1] is not None else sys.maxsize
|
||||
q_avail[quota] -= 1
|
||||
if q_avail[quota] < 0:
|
||||
avail = quota.availability(now_dt)
|
||||
if avail[0] != Quota.AVAILABILITY_OK:
|
||||
# This quota is sold out/currently unavailable, so do not sell this at all
|
||||
err = err or error_messages['unavailable']
|
||||
quota_ok = False
|
||||
break
|
||||
|
||||
if not quota_ok:
|
||||
if quota_ok:
|
||||
cp.expires = now_dt + timedelta(
|
||||
minutes=event.settings.get('reservation_time', as_type=int))
|
||||
cp.save()
|
||||
else:
|
||||
# Sorry, can't let you keep that!
|
||||
delete(cp)
|
||||
|
||||
# Check prices
|
||||
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
|
||||
old_total = sum(cp.price for cp in sorted_positions)
|
||||
for i, cp in enumerate(sorted_positions):
|
||||
if cp.listed_price is None:
|
||||
# migration from pre-discount cart positions
|
||||
cp.update_listed_price_and_voucher(max_discount=None)
|
||||
cp.migrate_free_price_if_necessary()
|
||||
|
||||
# deal with max discount
|
||||
max_discount = None
|
||||
if cp.voucher and cp.voucher.budget is not None:
|
||||
if cp.voucher not in v_budget:
|
||||
v_budget[cp.voucher] = cp.voucher.budget - cp.voucher.budget_used()
|
||||
max_discount = max(v_budget[cp.voucher], 0)
|
||||
|
||||
if cp.expires < now_dt or cp.listed_price is None:
|
||||
# Guarantee on listed price is expired
|
||||
cp.update_listed_price_and_voucher(max_discount=max_discount)
|
||||
elif cp.voucher:
|
||||
cp.update_listed_price_and_voucher(max_discount=max_discount, voucher_only=True)
|
||||
|
||||
if max_discount is not None:
|
||||
v_budget[cp.voucher] = v_budget[cp.voucher] - (cp.listed_price - cp.price_after_voucher)
|
||||
|
||||
try:
|
||||
cp.update_line_price(address, [b for b in sorted_positions if b.addon_to_id == cp.pk and b.is_bundled and b.pk and b.pk not in deleted_positions])
|
||||
except TaxRule.SaleNotAllowed:
|
||||
err = err or error_messages['country_blocked']
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
|
||||
discount_results = apply_discounts(
|
||||
event,
|
||||
sales_channel,
|
||||
[
|
||||
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
||||
for cp in sorted_positions
|
||||
]
|
||||
)
|
||||
for cp, (new_price, discount) in zip(sorted_positions, discount_results):
|
||||
if cp.price != new_price or cp.discount_id != (discount.pk if discount else None):
|
||||
cp.price = new_price
|
||||
cp.discount = discount
|
||||
cp.save(update_fields=['price', 'discount'])
|
||||
|
||||
new_total = sum(cp.price for cp in sorted_positions)
|
||||
if old_total != new_total:
|
||||
err = err or error_messages['price_changed']
|
||||
|
||||
# Store updated positions
|
||||
for cp in sorted_positions:
|
||||
cp.expires = now_dt + timedelta(
|
||||
minutes=event.settings.get('reservation_time', as_type=int))
|
||||
cp.save()
|
||||
|
||||
if err:
|
||||
raise OrderError(err, errargs)
|
||||
|
||||
@@ -1854,14 +1858,16 @@ class OrderChangeManager:
|
||||
op.position.item = op.item
|
||||
op.position.variation = op.variation
|
||||
op.position._calculate_tax()
|
||||
|
||||
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(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'))
|
||||
if op.position.price_before_voucher is not None and op.position.voucher and not op.position.addon_to_id:
|
||||
op.position.price_before_voucher = max(
|
||||
op.position.price,
|
||||
get_price(
|
||||
op.position.item, op.position.variation,
|
||||
subevent=op.position.subevent,
|
||||
custom_price=op.position.price,
|
||||
invoice_address=self._invoice_address
|
||||
).gross
|
||||
)
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate=False, save=False
|
||||
)
|
||||
@@ -1902,13 +1908,16 @@ class OrderChangeManager:
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate=False, save=False
|
||||
)
|
||||
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(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'))
|
||||
if op.position.price_before_voucher is not None and op.position.voucher and not op.position.addon_to_id:
|
||||
op.position.price_before_voucher = max(
|
||||
op.position.price,
|
||||
get_price(
|
||||
op.position.item, op.position.variation,
|
||||
subevent=op.position.subevent,
|
||||
custom_price=op.position.price,
|
||||
invoice_address=self._invoice_address
|
||||
).gross
|
||||
)
|
||||
op.position.save()
|
||||
elif isinstance(op, self.AddFeeOperation):
|
||||
self.order.log_action('pretix.event.order.changed.addfee', user=self.user, auth=self.auth, data={
|
||||
|
||||
@@ -20,30 +20,39 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import (
|
||||
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import Event, SubEvent
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
|
||||
|
||||
def get_price(item: Item, variation: ItemVariation = None,
|
||||
voucher: Voucher = None, custom_price: Decimal = None,
|
||||
subevent: SubEvent = None, custom_price_is_net: bool = False,
|
||||
custom_price_is_tax_rate: Decimal = None,
|
||||
custom_price_is_tax_rate: Decimal=None,
|
||||
addon_to: AbstractPosition = None, invoice_address: InvoiceAddress = None,
|
||||
force_custom_price: bool = False, bundled_sum: Decimal = Decimal('0.00'),
|
||||
max_discount: Decimal = None, tax_rule=None) -> TaxedPrice:
|
||||
if is_included_for_free(item, addon_to):
|
||||
return TAXED_ZERO
|
||||
if addon_to:
|
||||
try:
|
||||
iao = addon_to.item.addons.get(addon_category_id=item.category_id)
|
||||
if iao.price_included:
|
||||
return TAXED_ZERO
|
||||
except ItemAddOn.DoesNotExist:
|
||||
pass
|
||||
|
||||
price = get_listed_price(item, variation, subevent)
|
||||
price = item.default_price
|
||||
if subevent and item.pk in subevent.item_price_overrides:
|
||||
price = subevent.item_price_overrides[item.pk]
|
||||
|
||||
if variation is not None:
|
||||
if variation.default_price is not None:
|
||||
price = variation.default_price
|
||||
if subevent and variation.pk in subevent.var_price_overrides:
|
||||
price = subevent.var_price_overrides[variation.pk]
|
||||
|
||||
if voucher:
|
||||
price = voucher.calculate_price(price, max_discount=max_discount)
|
||||
@@ -76,10 +85,10 @@ def get_price(item: Item, variation: ItemVariation = None,
|
||||
price = tax_rule.tax(price, invoice_address=invoice_address)
|
||||
|
||||
if custom_price_is_net:
|
||||
price = tax_rule.tax(max(custom_price, price.net), base_price_is='net', override_tax_rate=price.rate,
|
||||
price = tax_rule.tax(max(custom_price, price.net), base_price_is='net',
|
||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
else:
|
||||
price = tax_rule.tax(max(custom_price, price.gross), base_price_is='gross', override_tax_rate=price.rate,
|
||||
price = tax_rule.tax(max(custom_price, price.gross), base_price_is='gross', gross_price_is_tax_rate=custom_price_is_tax_rate,
|
||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
else:
|
||||
price = tax_rule.tax(price, invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
@@ -89,83 +98,3 @@ def get_price(item: Item, variation: ItemVariation = None,
|
||||
price.tax = price.gross - price.net
|
||||
|
||||
return price
|
||||
|
||||
|
||||
def is_included_for_free(item: Item, addon_to: AbstractPosition):
|
||||
if addon_to:
|
||||
try:
|
||||
iao = addon_to.item.addons.get(addon_category_id=item.category_id)
|
||||
if iao.price_included:
|
||||
return True
|
||||
except ItemAddOn.DoesNotExist:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def get_listed_price(item: Item, variation: ItemVariation = None, subevent: SubEvent = None) -> Decimal:
|
||||
price = item.default_price
|
||||
if subevent and item.pk in subevent.item_price_overrides:
|
||||
price = subevent.item_price_overrides[item.pk]
|
||||
|
||||
if variation is not None:
|
||||
if variation.default_price is not None:
|
||||
price = variation.default_price
|
||||
if subevent and variation.pk in subevent.var_price_overrides:
|
||||
price = subevent.var_price_overrides[variation.pk]
|
||||
|
||||
return price
|
||||
|
||||
|
||||
def get_line_price(price_after_voucher: Decimal, custom_price_input: Decimal, custom_price_input_is_net: bool,
|
||||
tax_rule: TaxRule, invoice_address: InvoiceAddress, bundled_sum: Decimal) -> TaxedPrice:
|
||||
if not tax_rule:
|
||||
tax_rule = TaxRule(
|
||||
name='',
|
||||
rate=Decimal('0.00'),
|
||||
price_includes_tax=True,
|
||||
eu_reverse_charge=False,
|
||||
)
|
||||
if custom_price_input:
|
||||
price = tax_rule.tax(price_after_voucher, invoice_address=invoice_address)
|
||||
|
||||
if custom_price_input_is_net:
|
||||
price = tax_rule.tax(max(custom_price_input, price.net), base_price_is='net', override_tax_rate=price.rate,
|
||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
else:
|
||||
price = tax_rule.tax(max(custom_price_input, price.gross), base_price_is='gross', override_tax_rate=price.rate,
|
||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
else:
|
||||
price = tax_rule.tax(price_after_voucher, invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
|
||||
return price
|
||||
|
||||
|
||||
def apply_discounts(event: Event, sales_channel: str,
|
||||
positions: List[Tuple[int, Optional[int], Decimal, bool, bool]]) -> List[Decimal]:
|
||||
"""
|
||||
Applies any dynamic discounts to a cart
|
||||
|
||||
:param event: Event the cart belongs to
|
||||
:param sales_channel: Sales channel the cart was created with
|
||||
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
|
||||
:return: A list of ``(new_gross_price, discount)`` tuples in the same order as the input
|
||||
"""
|
||||
new_prices = {}
|
||||
|
||||
discount_qs = event.discounts.filter(
|
||||
Q(available_from__isnull=True) | Q(available_from__lte=now()),
|
||||
Q(available_until__isnull=True) | Q(available_until__gte=now()),
|
||||
sales_channels__contains=sales_channel,
|
||||
active=True,
|
||||
).prefetch_related('condition_limit_products').order_by('position', 'pk')
|
||||
for discount in discount_qs:
|
||||
result = discount.apply({
|
||||
idx: (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
|
||||
})
|
||||
for k in result.keys():
|
||||
result[k] = (result[k], discount)
|
||||
new_prices.update(result)
|
||||
|
||||
return [new_prices.get(idx, (p[2], None)) for idx, p in enumerate(positions)]
|
||||
|
||||
@@ -217,30 +217,6 @@ def timeline_for_event(event, subevent=None):
|
||||
})
|
||||
))
|
||||
|
||||
for d in event.discounts.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)):
|
||||
if d.available_from:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=d.available_from,
|
||||
description=pgettext_lazy('timeline', 'Discount "{name}" becomes active').format(name=str(d)),
|
||||
edit_url=reverse('control:event.items.discounts.edit', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'discount': d.pk,
|
||||
})
|
||||
))
|
||||
if d.available_until:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=d.available_until,
|
||||
description=pgettext_lazy('timeline', 'Discount "{name}" becomes inactive').format(name=str(d)),
|
||||
edit_url=reverse('control:event.items.discounts.edit', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'discount': d.pk,
|
||||
})
|
||||
))
|
||||
|
||||
for p in event.items.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)):
|
||||
if p.available_from:
|
||||
tl.append(TimelineEvent(
|
||||
|
||||
@@ -1,109 +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 decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||
from pretix.base.models import Discount
|
||||
from pretix.control.forms import ItemMultipleChoiceField, SplitDateTimeField
|
||||
|
||||
|
||||
class DiscountForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = Discount
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'active',
|
||||
'internal_name',
|
||||
'sales_channels',
|
||||
'available_from',
|
||||
'available_until',
|
||||
'subevent_mode',
|
||||
'condition_all_products',
|
||||
'condition_limit_products',
|
||||
'condition_min_count',
|
||||
'condition_min_value',
|
||||
'condition_apply_to_addons',
|
||||
'condition_ignore_voucher_discounted',
|
||||
'benefit_discount_matching_percent',
|
||||
'benefit_only_apply_to_cheapest_n_matches',
|
||||
]
|
||||
field_classes = {
|
||||
'available_from': SplitDateTimeField,
|
||||
'available_until': SplitDateTimeField,
|
||||
'condition_limit_products': ItemMultipleChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'subevent_mode': forms.RadioSelect,
|
||||
'available_from': SplitDateTimePickerWidget(),
|
||||
'available_until': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_available_from_0'}),
|
||||
'condition_limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '<[name$=all_products]',
|
||||
'class': 'scrolling-multiple-choice',
|
||||
}),
|
||||
'benefit_only_apply_to_cheapest_n_matches': forms.NumberInput(
|
||||
attrs={
|
||||
'data-display-dependency': '#id_condition_min_count',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs['event']
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['sales_channels'] = forms.MultipleChoiceField(
|
||||
label=_('Sales channels'),
|
||||
required=True,
|
||||
choices=(
|
||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
||||
if c.discounts_supported
|
||||
),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
)
|
||||
self.fields['condition_limit_products'].queryset = self.event.items.all()
|
||||
self.fields['condition_min_count'].required = False
|
||||
self.fields['condition_min_count'].widget.is_required = False
|
||||
self.fields['condition_min_value'].required = False
|
||||
self.fields['condition_min_value'].widget.is_required = False
|
||||
|
||||
if not self.event.has_subevents:
|
||||
del self.fields['subevent_mode']
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d.get('condition_min_value') and d.get('benefit_only_apply_to_cheapest_n_matches'):
|
||||
# field is hidden by JS
|
||||
d['benefit_only_apply_to_cheapest_n_matches'] = None
|
||||
if d.get('subevent_mode') == Discount.SUBEVENT_MODE_DISTINCT and d.get('condition_min_value'):
|
||||
# field is hidden by JS
|
||||
d['condition_min_value'] = Decimal('0.00')
|
||||
|
||||
if d.get('condition_min_count') is None:
|
||||
d['condition_min_count'] = 0
|
||||
if d.get('condition_min_value') is None:
|
||||
d['condition_min_value'] = Decimal('0.00')
|
||||
return d
|
||||
@@ -1265,8 +1265,7 @@ class CustomerFilterForm(FilterForm):
|
||||
orders = {
|
||||
'email': 'email',
|
||||
'identifier': 'identifier',
|
||||
'name': 'name_cached',
|
||||
'external_identifier': 'external_identifier',
|
||||
'name_cached': 'name_cached',
|
||||
}
|
||||
query = forms.CharField(
|
||||
label=_('Search query'),
|
||||
@@ -1310,8 +1309,6 @@ class CustomerFilterForm(FilterForm):
|
||||
Q(email__icontains=query)
|
||||
| Q(name_cached__icontains=query)
|
||||
| Q(identifier__istartswith=query)
|
||||
| Q(external_identifier__icontains=query)
|
||||
| Q(notes__icontains=query)
|
||||
)
|
||||
|
||||
if fdata.get('status') == 'active':
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import copy
|
||||
import os
|
||||
from decimal import Decimal
|
||||
from urllib.parse import urlencode
|
||||
@@ -424,10 +423,9 @@ class ItemCreateForm(I18nModelForm):
|
||||
if self.cleaned_data.get('has_variations'):
|
||||
if self.cleaned_data.get('copy_from') and self.cleaned_data.get('copy_from').has_variations:
|
||||
for variation in self.cleaned_data['copy_from'].variations.all():
|
||||
v = copy.copy(variation)
|
||||
v.pk = None
|
||||
v.item = instance
|
||||
v.save()
|
||||
ItemVariation.objects.create(item=instance, value=variation.value, active=variation.active,
|
||||
position=variation.position, default_price=variation.default_price,
|
||||
description=variation.description, original_price=variation.original_price)
|
||||
else:
|
||||
ItemVariation.objects.create(
|
||||
item=instance, value=__('Standard')
|
||||
|
||||
@@ -607,7 +607,7 @@ class CustomerUpdateForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = ['is_active', 'external_identifier', 'name_parts', 'email', 'is_verified', 'phone', 'locale', 'notes']
|
||||
fields = ['is_active', 'name_parts', 'email', 'is_verified', 'phone', 'locale']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -651,7 +651,7 @@ class CustomerCreateForm(CustomerUpdateForm):
|
||||
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = ['is_active', 'identifier', 'external_identifier', 'name_parts', 'email', 'is_verified', 'phone', 'locale', 'notes']
|
||||
fields = ['identifier', 'is_active', 'name_parts', 'email', 'is_verified', 'locale']
|
||||
|
||||
|
||||
class MembershipUpdateForm(forms.ModelForm):
|
||||
|
||||
@@ -449,9 +449,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.question.added': _('The question has been added.'),
|
||||
'pretix.event.question.deleted': _('The question has been deleted.'),
|
||||
'pretix.event.question.changed': _('The question has been changed.'),
|
||||
'pretix.event.discount.added': _('The discount has been added.'),
|
||||
'pretix.event.discount.deleted': _('The discount has been deleted.'),
|
||||
'pretix.event.discount.changed': _('The discount has been changed.'),
|
||||
'pretix.event.taxrule.added': _('The tax rule has been added.'),
|
||||
'pretix.event.taxrule.deleted': _('The tax rule has been deleted.'),
|
||||
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),
|
||||
|
||||
@@ -186,14 +186,6 @@ def get_event_navigation(request: HttpRequest):
|
||||
}),
|
||||
'active': 'event.items.questions' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Discounts'),
|
||||
'url': reverse('control:event.items.discounts', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.items.discounts' in url.url_name,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -80,17 +80,13 @@
|
||||
{% elif c.forced and c.successful %}
|
||||
<span class="fa fa-fw fa-warning" data-toggle="tooltip"
|
||||
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}"></span>
|
||||
{% elif c.force_sent %}
|
||||
<span class="fa fa-fw fa-cloud-upload" data-toggle="tooltip"
|
||||
title="{% blocktrans trimmed with date=c.created|date:'SHORT_DATETIME_FORMAT' %}Offline scan. Upload time: {{ date }}{% endblocktrans %}"></span>
|
||||
{% elif c.forced and not c.successful %}
|
||||
<br>
|
||||
<small class="text-muted">{% trans "Failed in offline mode" %}</small>
|
||||
{% elif c.auto_checked_in %}
|
||||
<span class="fa fa-fw fa-magic" data-toggle="tooltip"
|
||||
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}"></span>
|
||||
{% endif %}
|
||||
{% if c.forced and not c.successful %}
|
||||
<br>
|
||||
<small class="text-muted">{% trans "Failed in offline mode" %}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.type == "exit" %}<span class="fa fa-fw fa-sign-out"></span>{% endif %}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Automatic discount" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Automatic discount" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12{% if discount %} col-lg-10{% endif %}">
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.active layout="control" %}
|
||||
{% bootstrap_field form.internal_name layout="control" %}
|
||||
{% bootstrap_field form.available_from layout="control" %}
|
||||
{% bootstrap_field form.available_until layout="control" %}
|
||||
{% bootstrap_field form.sales_channels layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Condition" context "discount" %}</legend>
|
||||
{% bootstrap_field form.condition_all_products layout="control" %}
|
||||
{% bootstrap_field form.condition_limit_products layout="control" %}
|
||||
{% bootstrap_field form.condition_apply_to_addons layout="control" %}
|
||||
{% bootstrap_field form.condition_ignore_voucher_discounted layout="control" %}
|
||||
{% if form.subevent_mode %}
|
||||
{% bootstrap_field form.subevent_mode layout="control" %}
|
||||
{% endif %}
|
||||
<div class="form-group form-alternatives">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Minimum cart content" %}<br>
|
||||
<span class="optional">{% trans "Optional" %}</span>
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.condition_min_count form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-1 text-center condition-or" data-display-dependency="#id_subevent_mode_2" data-inverse>
|
||||
<div class="hr">
|
||||
<div class="sep">
|
||||
<div class="sepText">{% trans "OR" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4" data-display-dependency="#id_subevent_mode_2" data-inverse>
|
||||
{% bootstrap_field form.condition_min_value form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Benefit" context "discount" %}</legend>
|
||||
{% bootstrap_field form.benefit_discount_matching_percent layout="control" addon_after="%" %}
|
||||
{% bootstrap_field form.benefit_only_apply_to_cheapest_n_matches layout="control" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% if discount %}
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Discount history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=discount %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,41 +0,0 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Delete discount" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Delete discount" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if not possible and not item.active %}
|
||||
<p>{% blocktrans %}You cannot delete the discount <strong>{{ discount }}</strong> because it already has
|
||||
been used as part of an order.{% endblocktrans %}</p>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.discounts" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if possible %}
|
||||
<p>{% blocktrans trimmed with name=discount.internal_name %}
|
||||
Are you sure you want to delete the discount <strong>{{ name }}</strong>?
|
||||
{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
<p>{% blocktrans trimmed with name=discount.internal_name %}
|
||||
You cannot delete the discount <strong>{{ name }}</strong> because it already has been used as part
|
||||
of an order, but you can deactivate it.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.items.discounts" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% if possible %}{% trans "Delete" %}{% else %}{% trans "Deactivate" %}{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,147 +0,0 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Automatic discounts" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Automatic discounts" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
With automatic discounts, you can automatically apply a discount to purchases from your customers based
|
||||
on certain conditions. For example, you can create group discounts like "get 20% off if you buy 3 or more
|
||||
tickets" or "buy 2 tickets, get 1 free".
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Automatic discounts are available to all customers as long as they are active. If you want to offer special
|
||||
prices only to specific customers, you can use vouchers instead. If you want to offer discounts across
|
||||
multiple purchases ("buy a package of 10 you can turn into individual tickets later"), you can use
|
||||
customer accounts and memberships instead.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Discounts are only automatically applied during an initial purchase. They are not applied if an existing
|
||||
order is changed through any of the available options.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Every product in the cart can only be affected by one discount. If you have overlapping discounts, the
|
||||
first one in the order of the list below will apply.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if discounts|length == 0 %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You haven't created any discounts yet.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new discount" %}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new discount" %}
|
||||
</a>
|
||||
</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Internal name" %}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>{% trans "Products" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-dnd-url="{% url "control:event.items.discounts.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
{% for d in discounts %}
|
||||
<tr data-dnd-id="{{ d.id }}">
|
||||
<td>
|
||||
{% if d.active %}
|
||||
<strong>
|
||||
{% else %}
|
||||
<del>
|
||||
{% endif %}
|
||||
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}">
|
||||
{{ d.internal_name }}</a>
|
||||
{% if d.active %}
|
||||
</strong>
|
||||
{% else %}
|
||||
</del>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% for k, c in sales_channels.items %}
|
||||
{% if k in d.sales_channels %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.available_from or d.available_until %}
|
||||
{% if not d.is_available_by_time %}
|
||||
<span class="label label-danger" data-toggle="tooltip"
|
||||
title="{% trans "Currently unavailable since a limited timeframe for this product has been set" %}">
|
||||
<span class="fa fa-clock-o fa-fw" data-toggle="tooltip">
|
||||
</span>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="fa fa-clock-o fa-fw text-muted" data-toggle="tooltip"
|
||||
title="{% trans "Only available in a limited timeframe" %}">
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.condition_all_products %}
|
||||
<em>{% trans "All" %}</em>
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for item in d.condition_limit_products.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||
class="btn btn-default btn-sm sortable-up"
|
||||
{% if forloop.counter0 == 0 and not page_obj.has_previous %}
|
||||
disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
|
||||
<button formaction="{% url "control:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||
class="btn btn-default btn-sm sortable-down"
|
||||
{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<span class="dnd-container"></span>
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}"
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.items.discounts.delete" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -387,7 +387,7 @@
|
||||
{% if line.voucher %}
|
||||
<br/><span class="fa fa-tags fa-fw"></span> {% trans "Voucher code used:" %}
|
||||
<a
|
||||
{% if line.voucher.budget and line.voucher_budget_use|default_if_none:"NONE" != "NONE" %}data-toggle="tooltip" title="{% blocktrans trimmed with amount=line.voucher_budget_use|money:request.event.currency %}Used {{ amount }} discount from budget{% endblocktrans %}"{% endif %}
|
||||
{% if line.price_before_voucher|default_if_none:"NONE" != "NONE" %}data-toggle="tooltip" title="{% blocktrans trimmed with price=line.price_before_voucher|money:request.event.currency %}Original price: {{ price }}{% endblocktrans %}"{% endif %}
|
||||
href="{% url "control:event.voucher" event=request.event.slug organizer=request.event.organizer.slug voucher=line.voucher.pk %}">
|
||||
{{ line.voucher.code }}
|
||||
</a>
|
||||
@@ -406,15 +406,6 @@
|
||||
{{ line.used_membership }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if line.discount %}
|
||||
<br />
|
||||
<span class="text-success discounted" data-toggle="tooltip" title="{% trans "The price of this product was reduced because of an automatic discount or this product was part of the discount calculation for a different product in this order." %}">
|
||||
<span class="fa fa-percent fa-fw" aria-hidden="true"></span>
|
||||
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=line.discount.id %}">
|
||||
{{ line.discount.internal_name }}
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if not line.canceled %}
|
||||
<div class="position-buttons">
|
||||
{% if line.generate_ticket %}
|
||||
|
||||
@@ -27,10 +27,6 @@
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Customer ID" %}</dt>
|
||||
<dd>#{{ customer.identifier }}</dd>
|
||||
{% if customer.external_identifier %}
|
||||
<dt>{% trans "External identifier" %}</dt>
|
||||
<dd>{{ customer.external_identifier }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>
|
||||
{% if not customer.is_active %}
|
||||
@@ -63,10 +59,6 @@
|
||||
<dt>{% trans "Last login" %}</dt>
|
||||
<dd>{% if customer.last_login %}{{ customer.last_login|date:"SHORT_DATETIME_FORMAT" }}{% else %}
|
||||
–{% endif %}</dd>
|
||||
{% if customer.notes %}
|
||||
<dt>{% trans "Notes" %}</dt>
|
||||
<dd>{{ customer.notes|linebreaks }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</form>
|
||||
<div class="text-right">
|
||||
|
||||
@@ -62,9 +62,6 @@
|
||||
<th>{% trans "Name" %}
|
||||
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "External identifier" %}
|
||||
<a href="?{% url_replace request 'ordering' '-external_identifier' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'external_identifier' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -84,7 +81,6 @@
|
||||
{% if not c.is_verified %}</strike>{% endif %}
|
||||
</td>
|
||||
<td>{{ c.name }}</td>
|
||||
<td>{% if c.external_identifier %}{{ c.external_identifier }}{% endif %}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=c.identifier %}"
|
||||
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Details" %}">
|
||||
|
||||
@@ -37,9 +37,9 @@ from django.conf.urls import include, re_path
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
from pretix.control.views import (
|
||||
auth, checkin, dashboards, discounts, event, geo, global_settings, item,
|
||||
main, oauth, orderimport, orders, organizer, pdf, search, shredder,
|
||||
subevents, typeahead, user, users, vouchers, waitinglist,
|
||||
auth, checkin, dashboards, event, geo, global_settings, item, main, oauth,
|
||||
orderimport, orders, organizer, pdf, search, shredder, subevents,
|
||||
typeahead, user, users, vouchers, waitinglist,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -279,16 +279,6 @@ urlpatterns = [
|
||||
re_path(r'^quotas/(?P<quota>\d+)/delete$', item.QuotaDelete.as_view(),
|
||||
name='event.items.quotas.delete'),
|
||||
re_path(r'^quotas/add$', item.QuotaCreate.as_view(), name='event.items.quotas.add'),
|
||||
re_path(r'^discounts/$', discounts.DiscountList.as_view(), name='event.items.discounts'),
|
||||
re_path(r'^discounts/(?P<discount>\d+)/delete$', discounts.DiscountDelete.as_view(),
|
||||
name='event.items.discounts.delete'),
|
||||
re_path(r'^discounts/(?P<discount>\d+)/up$', discounts.discount_move_up, name='event.items.discounts.up'),
|
||||
re_path(r'^discounts/(?P<discount>\d+)/down$', discounts.discount_move_down,
|
||||
name='event.items.discounts.down'),
|
||||
re_path(r'^discounts/reorder$', discounts.reorder_discounts, name='event.items.discounts.reorder'),
|
||||
re_path(r'^discounts/(?P<discount>\d+)/$', discounts.DiscountUpdate.as_view(),
|
||||
name='event.items.discounts.edit'),
|
||||
re_path(r'^discounts/add$', discounts.DiscountCreate.as_view(), name='event.items.discounts.add'),
|
||||
re_path(r'^vouchers/$', vouchers.VoucherList.as_view(), name='event.vouchers'),
|
||||
re_path(r'^vouchers/tags/$', vouchers.VoucherTags.as_view(), name='event.vouchers.tags'),
|
||||
re_path(r'^vouchers/rng$', vouchers.VoucherRNG.as_view(), name='event.vouchers.rng'),
|
||||
|
||||
@@ -1,269 +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/>.
|
||||
#
|
||||
|
||||
import json
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.db.models import Max
|
||||
from django.http import (
|
||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
||||
)
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic.edit import DeleteView
|
||||
|
||||
from pretix.base.models import CartPosition, Discount
|
||||
from pretix.control.forms.discounts import DiscountForm
|
||||
from pretix.control.permissions import (
|
||||
EventPermissionRequiredMixin, event_permission_required,
|
||||
)
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
from ...base.channels import get_all_sales_channels
|
||||
from . import CreateView, PaginationMixin, UpdateView
|
||||
|
||||
|
||||
class DiscountDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Discount
|
||||
template_name = 'pretixcontrol/items/discount_delete.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'discount'
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['possible'] = self.object.allow_delete()
|
||||
return context
|
||||
|
||||
def get_object(self, queryset=None) -> Discount:
|
||||
try:
|
||||
return self.request.event.discounts.get(
|
||||
id=self.kwargs['discount']
|
||||
)
|
||||
except Discount.DoesNotExist:
|
||||
raise Http404(_("The requested discount does not exist."))
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
success_url = self.get_success_url()
|
||||
if self.object.allow_delete():
|
||||
CartPosition.objects.filter(discount=self.object).update(discount=None)
|
||||
self.object.log_action('pretix.event.discount.deleted', user=self.request.user)
|
||||
self.object.delete()
|
||||
messages.success(request, _('The selected discount has been deleted.'))
|
||||
else:
|
||||
o = self.get_object()
|
||||
o.active = False
|
||||
o.save()
|
||||
o.log_action('pretix.event.discount.changed', user=self.request.user, data={
|
||||
'active': False
|
||||
})
|
||||
messages.success(request, _('The selected discount has been deactivated.'))
|
||||
return HttpResponseRedirect(success_url)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.discounts', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
|
||||
class DiscountUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
model = Discount
|
||||
form_class = DiscountForm
|
||||
template_name = 'pretixcontrol/items/discount.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'discount'
|
||||
|
||||
def get_object(self, queryset=None) -> Discount:
|
||||
url = resolve(self.request.path_info)
|
||||
try:
|
||||
return self.request.event.discounts.get(
|
||||
id=url.kwargs['discount']
|
||||
)
|
||||
except Discount.DoesNotExist:
|
||||
raise Http404(_("The requested discount does not exist."))
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
if form.has_changed():
|
||||
self.object.log_action(
|
||||
'pretix.event.discount.changed', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.discounts', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['event'] = self.request.event
|
||||
return kwargs
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class DiscountCreate(EventPermissionRequiredMixin, CreateView):
|
||||
model = Discount
|
||||
form_class = DiscountForm
|
||||
template_name = 'pretixcontrol/items/discount.html'
|
||||
permission = 'can_change_items'
|
||||
context_object_name = 'discount'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.discounts', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
@cached_property
|
||||
def copy_from(self):
|
||||
if self.request.GET.get("copy_from") and not getattr(self, 'object', None):
|
||||
try:
|
||||
return self.request.event.discounts.get(pk=self.request.GET.get("copy_from"))
|
||||
except Discount.DoesNotExist:
|
||||
pass
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
|
||||
if self.copy_from:
|
||||
i = modelcopy(self.copy_from)
|
||||
i.pk = None
|
||||
kwargs['instance'] = i
|
||||
else:
|
||||
kwargs['instance'] = Discount(event=self.request.event)
|
||||
|
||||
kwargs['event'] = self.request.event
|
||||
return kwargs
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.event = self.request.event
|
||||
form.instance.position = (self.request.event.discounts.aggregate(m=Max('position'))['m'] or 0) + 1
|
||||
messages.success(self.request, _('The new discount has been created.'))
|
||||
ret = super().form_valid(form)
|
||||
form.instance.log_action('pretix.event.discount.added', data=dict(form.cleaned_data), user=self.request.user)
|
||||
return ret
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class DiscountList(PaginationMixin, ListView):
|
||||
model = Discount
|
||||
context_object_name = 'discounts'
|
||||
template_name = 'pretixcontrol/items/discounts.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.discounts.prefetch_related('condition_limit_products')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['sales_channels'] = get_all_sales_channels()
|
||||
return ctx
|
||||
|
||||
|
||||
def discount_move(request, discount, up=True):
|
||||
"""
|
||||
This is a helper function to avoid duplicating code in discount_move_up and
|
||||
discount_move_down. It takes a discount and a direction and then tries to bring
|
||||
all discounts for this event in a new order.
|
||||
"""
|
||||
try:
|
||||
discount = request.event.discounts.get(
|
||||
id=discount
|
||||
)
|
||||
except Discount.DoesNotExist:
|
||||
raise Http404(_("The requested discount does not exist."))
|
||||
discounts = list(request.event.discounts.order_by("position"))
|
||||
|
||||
index = discounts.index(discount)
|
||||
if index != 0 and up:
|
||||
discounts[index - 1], discounts[index] = discounts[index], discounts[index - 1]
|
||||
elif index != len(discounts) - 1 and not up:
|
||||
discounts[index + 1], discounts[index] = discounts[index], discounts[index + 1]
|
||||
|
||||
for i, d in enumerate(discounts):
|
||||
if d.position != i:
|
||||
d.position = i
|
||||
d.save()
|
||||
messages.success(request, _('The order of discounts has been updated.'))
|
||||
|
||||
|
||||
@event_permission_required("can_change_items")
|
||||
@require_http_methods(["POST"])
|
||||
def discount_move_up(request, organizer, event, discount):
|
||||
discount_move(request, discount, up=True)
|
||||
return redirect('control:event.items.discounts',
|
||||
organizer=request.event.organizer.slug,
|
||||
event=request.event.slug)
|
||||
|
||||
|
||||
@event_permission_required("can_change_items")
|
||||
@require_http_methods(["POST"])
|
||||
def discount_move_down(request, organizer, event, discount):
|
||||
discount_move(request, discount, up=False)
|
||||
return redirect('control:event.items.discounts',
|
||||
organizer=request.event.organizer.slug,
|
||||
event=request.event.slug)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
@event_permission_required("can_change_items")
|
||||
@require_http_methods(["POST"])
|
||||
def reorder_discounts(request, organizer, event):
|
||||
try:
|
||||
ids = json.loads(request.body.decode('utf-8'))['ids']
|
||||
except (JSONDecodeError, KeyError, ValueError):
|
||||
return HttpResponseBadRequest("expected JSON: {ids:[]}")
|
||||
|
||||
input_discounts = list(request.event.discounts.filter(id__in=[i for i in ids if i.isdigit()]))
|
||||
|
||||
if len(input_discounts) != len(ids):
|
||||
raise Http404(_("Some of the provided object ids are invalid."))
|
||||
|
||||
if len(input_discounts) != request.event.discounts.count():
|
||||
raise Http404(_("Not all discounts have been selected."))
|
||||
|
||||
for c in input_discounts:
|
||||
pos = ids.index(str(c.pk))
|
||||
if pos != c.position: # Save unneccessary UPDATE queries
|
||||
c.position = pos
|
||||
c.save(update_fields=['position'])
|
||||
|
||||
return HttpResponse()
|
||||
@@ -169,7 +169,7 @@ def reorder_items(request, organizer, event):
|
||||
input_items = list(request.event.items.filter(id__in=[i for i in ids if i.isdigit()]))
|
||||
|
||||
if len(input_items) != len(ids):
|
||||
raise Http404(_("Some of the provided object ids are invalid."))
|
||||
raise Http404(_("Some of the provided item ids are invalid."))
|
||||
|
||||
item_categories = {i.category_id for i in input_items}
|
||||
if len(item_categories) > 1:
|
||||
@@ -178,7 +178,7 @@ def reorder_items(request, organizer, event):
|
||||
# get first and only category
|
||||
item_category = next(iter(item_categories))
|
||||
if len(input_items) != request.event.items.filter(category=item_category).count():
|
||||
raise Http404(_("Not all objects have been selected."))
|
||||
raise Http404(_("Not all items have been selected."))
|
||||
|
||||
for i in input_items:
|
||||
pos = ids.index(str(i.pk))
|
||||
@@ -372,10 +372,10 @@ def reorder_categories(request, organizer, event):
|
||||
input_categories = list(request.event.categories.filter(id__in=[i for i in ids if i.isdigit()]))
|
||||
|
||||
if len(input_categories) != len(ids):
|
||||
raise Http404(_("Some of the provided object ids are invalid."))
|
||||
raise Http404(_("Some of the provided category ids are invalid."))
|
||||
|
||||
if len(input_categories) != request.event.categories.count():
|
||||
raise Http404(_("Not all objects have been selected."))
|
||||
raise Http404(_("Not all categories have been selected."))
|
||||
|
||||
for c in input_categories:
|
||||
pos = ids.index(str(c.pk))
|
||||
@@ -501,10 +501,10 @@ def reorder_questions(request, organizer, event):
|
||||
input_questions = list(request.event.questions.filter(id__in=custom_question_ids))
|
||||
|
||||
if len(input_questions) != len(custom_question_ids):
|
||||
raise Http404(_("Some of the provided object ids are invalid."))
|
||||
raise Http404(_("Some of the provided question ids are invalid."))
|
||||
|
||||
if len(input_questions) != request.event.questions.count():
|
||||
raise Http404(_("Not all objects have been selected."))
|
||||
raise Http404(_("Not all questions have been selected."))
|
||||
|
||||
for q in input_questions:
|
||||
pos = ids.index(str(q.pk))
|
||||
|
||||
@@ -360,8 +360,7 @@ class OrderDetail(OrderView):
|
||||
cartpos = queryset.order_by(
|
||||
'item', 'variation'
|
||||
).select_related(
|
||||
'item', 'variation', 'addon_to', 'tax_rule', 'used_membership', 'used_membership__membership_type',
|
||||
'discount',
|
||||
'item', 'variation', 'addon_to', 'tax_rule', 'used_membership', 'used_membership__membership_type'
|
||||
).prefetch_related(
|
||||
'item__questions', 'issued_gift_cards',
|
||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
|
||||
|
||||
@@ -1,36 +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.template.loaders.app_directories import Loader
|
||||
from django.template.utils import get_app_template_dirs
|
||||
|
||||
|
||||
class AppLoader(Loader):
|
||||
|
||||
def get_dirs(self):
|
||||
ds = get_app_template_dirs('templates')
|
||||
ignore_patterns = {
|
||||
# Ignore templates of plugins we don't actually use as they cause trouble during
|
||||
# static file compression
|
||||
'/django_filters/',
|
||||
'/django_otp/',
|
||||
}
|
||||
return [d for d in ds if not any(p in str(d) for p in ignore_patterns)]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-05-27 13:57+0000\n"
|
||||
"POT-Creation-Date: 2022-04-26 16:23+0000\n"
|
||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -207,7 +207,7 @@ msgid "close"
|
||||
msgstr "إغلاق"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
@@ -216,12 +216,12 @@ msgstr ""
|
||||
"اخترت."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr "طلبك قيد الانتظار وستتم معالجته قريبا."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -230,36 +230,36 @@ msgstr ""
|
||||
"وصل طلبك للخادم وننتظر تنفيذه. إذا استغرق الأمر أكثر من دقيقتين تواصل معنا "
|
||||
"أو عاود المحاولة مجددا."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:175
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "حدث خطأ من نوع {code}."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr "لم نتمكن من الاتصال بالخادم، لكن سنواصل المحاولة، رمز آخر خطأ: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:144
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "استغرقت الطلب فترة طويلة، الرجاء المحاولة مرة أخرى."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
msgstr ""
|
||||
"لا يمكننا الوصول إلى الخادم حاليا، حاول مرة أخرى من فضلك. رمز الخطأ : {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:208
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr "جاري معالجة طلبك …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -268,7 +268,7 @@ msgstr ""
|
||||
"نعمل الآن على ارسال طلبك إلى الخادم، إذا أستغرقت العملية أكثر من دقيقة، يرجى "
|
||||
"التحقق من اتصالك بالإنترنت ثم أعد تحميل الصفحة مرة أخرى."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:273
|
||||
#: pretix/static/pretixbase/js/asynctask.js:270
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr "أغلق الرسالة"
|
||||
@@ -374,48 +374,48 @@ msgstr "الدقائق"
|
||||
msgid "Check-in QR"
|
||||
msgstr "QR الدخول"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "لا يمكن تحميل ملف PDF الخلفية للأسباب التالية:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||
msgid "Group of objects"
|
||||
msgstr "مجموعة من العناصر"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Text object"
|
||||
msgstr "عنصر نص"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||
msgid "Barcode area"
|
||||
msgstr "منطقة باركود"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||
msgid "Image area"
|
||||
msgstr "منطقة صورة"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Powered by pretix"
|
||||
msgstr "مدعوم من pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Object"
|
||||
msgstr "عنصر"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Ticket design"
|
||||
msgstr "تصميم التذكرة"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||
msgid "Saving failed."
|
||||
msgstr "فشلت عملية الحفظ."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr "حصل خطأ أثناء رفع ملف PDF الخاص بك، يرجى المحاولة مرة أخرى."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr "هل تريد أن تغادر المحرر دون حفظ التعديلات؟"
|
||||
|
||||
@@ -543,20 +543,20 @@ msgstr "ستسترد %(currency)%(amount)"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:377
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:413
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||
msgid "required"
|
||||
msgstr "مطلوب"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:516
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:535
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||
msgid "Time zone:"
|
||||
msgstr "المنطقة الزمنية:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:526
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||
msgid "Your local time:"
|
||||
msgstr "التوقيت المحلي:"
|
||||
|
||||
@@ -836,11 +836,6 @@ msgstr "نوفمبر"
|
||||
msgid "December"
|
||||
msgstr "ديسمبر"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Yes"
|
||||
#~ msgid "eps"
|
||||
#~ msgstr "نعم"
|
||||
|
||||
#~ msgid "Lead Scan QR"
|
||||
#~ msgstr "قم بمسح QR"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-05-27 13:57+0000\n"
|
||||
"POT-Creation-Date: 2022-04-26 16:23+0000\n"
|
||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -199,61 +199,61 @@ msgid "close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
"browser and try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:175
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:144
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:208
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Estem processant la vostra sol·licitud …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
"page and try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:273
|
||||
#: pretix/static/pretixbase/js/asynctask.js:270
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr ""
|
||||
@@ -359,48 +359,48 @@ msgstr ""
|
||||
msgid "Check-in QR"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||
msgid "Group of objects"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Text object"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||
msgid "Barcode area"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||
msgid "Image area"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Powered by pretix"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Object"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Ticket design"
|
||||
msgstr "Disseny del tiquet"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||
msgid "Saving failed."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
|
||||
@@ -518,22 +518,22 @@ msgstr ""
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:377
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:413
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "required"
|
||||
msgstr "Cistella expirada"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:516
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:535
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:526
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-05-27 13:57+0000\n"
|
||||
"POT-Creation-Date: 2022-04-26 16:23+0000\n"
|
||||
"PO-Revision-Date: 2021-12-06 23:00+0000\n"
|
||||
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
|
||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -199,7 +199,7 @@ msgid "close"
|
||||
msgstr "zavřít"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
@@ -208,12 +208,12 @@ msgstr ""
|
||||
"to může trvat několik minut."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr "Váš požadavek byl vložem do fronty serveru a brzy bude zpracován."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -223,14 +223,14 @@ msgstr ""
|
||||
"Pokud to trvá více jak dvě minuty, prosím kontaktuje nás nebo se vraťte do "
|
||||
"vašeho prohlížeče a zkuste to znovu."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:175
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "Vyskytla se chyba {code}."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
@@ -238,12 +238,12 @@ msgstr ""
|
||||
"Momentálně nemůžeme kontaktovat server, ale stále se o to pokoušíme. "
|
||||
"Poslední chybový kód: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:144
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "Zpracování požadavku trvá příliš dlouho. Prosím zkuste to znovu."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
@@ -251,11 +251,11 @@ msgstr ""
|
||||
"Momentálně nemůžeme kontaktovat server. Prosím zkuste to znovu. Chybový kód: "
|
||||
"{code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:208
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Zpracováváme váš požadavek …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -265,7 +265,7 @@ msgstr ""
|
||||
"prosím zkontrolujte své internetové připojení a znovu načtěte stránku a "
|
||||
"zkuste to znovu."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:273
|
||||
#: pretix/static/pretixbase/js/asynctask.js:270
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr "Zavřít zprávu"
|
||||
@@ -371,48 +371,48 @@ msgstr "minuty"
|
||||
msgid "Check-in QR"
|
||||
msgstr "Check-in QR kód"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "Pozadí PDF nemohl být načten:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||
msgid "Group of objects"
|
||||
msgstr "Skupina objektů"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Text object"
|
||||
msgstr "Textový objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||
msgid "Barcode area"
|
||||
msgstr "Oblast s QR kódem"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||
msgid "Image area"
|
||||
msgstr "Oblast obrazu"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Powered by pretix"
|
||||
msgstr "Poháněno společností pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Object"
|
||||
msgstr "Objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Ticket design"
|
||||
msgstr "Design vstupenky"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||
msgid "Saving failed."
|
||||
msgstr "Uložení se nepodařilo."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr "Při nahrávání souboru PDF došlo k problému, zkuste to prosím znovu."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr "Opravdu chcete opustit editor bez uložení změn?"
|
||||
|
||||
@@ -536,20 +536,20 @@ msgstr "Dostanete %(currency)s %(amount)s zpět"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr "Zadejte částku, kterou si organizátor může ponechat."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:377
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "Zadejte prosím množství pro jeden z typů vstupenek."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:413
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||
msgid "required"
|
||||
msgstr "povinný"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:516
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:535
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||
msgid "Time zone:"
|
||||
msgstr "Časové pásmo:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:526
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||
msgid "Your local time:"
|
||||
msgstr "Místní čas:"
|
||||
|
||||
@@ -829,11 +829,6 @@ msgstr "Listopad"
|
||||
msgid "December"
|
||||
msgstr "Prosinec"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Yes"
|
||||
#~ msgid "eps"
|
||||
#~ msgstr "Ano"
|
||||
|
||||
#~ msgid "Lead Scan QR"
|
||||
#~ msgstr "Lead Scan QR kód"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-05-27 13:57+0000\n"
|
||||
"POT-Creation-Date: 2022-04-26 16:23+0000\n"
|
||||
"PO-Revision-Date: 2022-04-01 13:36+0000\n"
|
||||
"Last-Translator: Anna-itk <abc@aarhus.dk>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -214,7 +214,7 @@ msgid "close"
|
||||
msgstr "Luk"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Your request has been queued on the server and will now be processed. "
|
||||
@@ -227,7 +227,7 @@ msgstr ""
|
||||
"der gå op til et par minutter."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Your request has been queued on the server and will now be processed. "
|
||||
@@ -238,7 +238,7 @@ msgstr ""
|
||||
"der gå op til et par minutter."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -247,14 +247,14 @@ msgstr ""
|
||||
"Din forespørgsel er under behandling. Hvis der går mere end to minutter, så "
|
||||
"kontakt os eller gå tilbage og prøv igen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:175
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "Der er sket en fejl ({code})."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
@@ -262,14 +262,14 @@ msgstr ""
|
||||
"Vi kan ikke komme i kontakt med serveren, men prøver igen. Seneste fejlkode: "
|
||||
"{code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:144
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
#, fuzzy
|
||||
#| msgid "The request took to long. Please try again."
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "Forespørgselen tog for lang tid. Prøv venligst igen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
@@ -277,11 +277,11 @@ msgstr ""
|
||||
"Vi kan ikke komme i kontakt med serveren. Prøv venligst igen. Fejlkode: "
|
||||
"{code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:208
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Vi behandler din bestilling …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -290,7 +290,7 @@ msgstr ""
|
||||
"Din forespørgsel bliver sendt til serveren. Hvis det tager mere end et "
|
||||
"minut, så tjek din internetforbindelse, genindlæs siden og prøv igen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:273
|
||||
#: pretix/static/pretixbase/js/asynctask.js:270
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr "Luk besked"
|
||||
@@ -399,50 +399,50 @@ msgstr ""
|
||||
msgid "Check-in QR"
|
||||
msgstr "Check-in QR"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "Baggrunds-pdf'en kunne ikke hentes af følgende grund:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||
msgid "Group of objects"
|
||||
msgstr "Gruppe af objekter"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Text object"
|
||||
msgstr "Tekstobjekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||
msgid "Barcode area"
|
||||
msgstr "QR-kode-område"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||
#, fuzzy
|
||||
#| msgid "Barcode area"
|
||||
msgid "Image area"
|
||||
msgstr "QR-kode-område"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Powered by pretix"
|
||||
msgstr "Drevet af pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Object"
|
||||
msgstr "Objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Ticket design"
|
||||
msgstr "Billetdesign"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||
msgid "Saving failed."
|
||||
msgstr "Gem fejlede."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr "Fejl under upload af pdf. Prøv venligt igen."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
"Er du sikker på at du vil forlade editoren uden at gemme dine ændringer?"
|
||||
@@ -571,22 +571,22 @@ msgstr "fra %(currency)s %(price)s"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:377
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:413
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "required"
|
||||
msgstr "Kurv udløbet"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:516
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:535
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||
msgid "Time zone:"
|
||||
msgstr "Tidszone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:526
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||
msgid "Your local time:"
|
||||
msgstr "Din lokaltid:"
|
||||
|
||||
@@ -867,11 +867,6 @@ msgstr "November"
|
||||
msgid "December"
|
||||
msgstr "December"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Yes"
|
||||
#~ msgid "eps"
|
||||
#~ msgstr "Ja"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Check-in QR"
|
||||
#~ msgid "Check-in result"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-05-27 13:57+0000\n"
|
||||
"PO-Revision-Date: 2022-04-28 18:04+0000\n"
|
||||
"POT-Creation-Date: 2022-04-26 16:23+0000\n"
|
||||
"PO-Revision-Date: 2021-10-25 15:40+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"de/>\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.12\n"
|
||||
"X-Generator: Weblate 4.8\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -199,7 +199,7 @@ msgid "close"
|
||||
msgstr "schließen"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
@@ -208,14 +208,14 @@ msgstr ""
|
||||
"einige Minuten dauern."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr ""
|
||||
"Ihre Anfrage befindet sich beim Server in der Warteschlange und wird bald "
|
||||
"verarbeitet."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -226,14 +226,14 @@ msgstr ""
|
||||
"bitte oder gehen Sie in Ihrem Browser einen Schritt zurück und versuchen es "
|
||||
"erneut."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:175
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
@@ -241,12 +241,12 @@ msgstr ""
|
||||
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
|
||||
"Letzter Fehlercode: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:144
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
@@ -254,11 +254,11 @@ msgstr ""
|
||||
"Wir können den Server aktuell nicht erreichen. Bitte versuchen Sie es noch "
|
||||
"einmal. Fehlercode: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:208
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Wir verarbeiten Ihre Anfrage …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -268,7 +268,7 @@ msgstr ""
|
||||
"dauert, prüfen Sie bitte Ihre Internetverbindung. Danach können Sie diese "
|
||||
"Seite neu laden und es erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:273
|
||||
#: pretix/static/pretixbase/js/asynctask.js:270
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr "Schließen"
|
||||
@@ -308,7 +308,7 @@ msgstr "Aktueller Zeitpunkt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:71
|
||||
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
|
||||
msgstr "Aktueller Tag der Woche (1 = Montag, 7 = Sonntag)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:75
|
||||
msgid "Number of previous entries"
|
||||
@@ -324,11 +324,11 @@ msgstr "Anzahl an Tagen mit vorherigem Eintritt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:87
|
||||
msgid "Minutes since last entry (-1 on first entry)"
|
||||
msgstr "Minuten seit vorherigem Eintritt (-1 bei erstem Zutritt)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:91
|
||||
msgid "Minutes since first entry (-1 on first entry)"
|
||||
msgstr "Minuten seit erstem Eintritt (-1 bei erstem Zutritt)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:112
|
||||
msgid "All of the conditions below (AND)"
|
||||
@@ -374,49 +374,49 @@ msgstr "Minuten"
|
||||
msgid "Check-in QR"
|
||||
msgstr "Check-in-QR-Code"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||
msgid "Group of objects"
|
||||
msgstr "Gruppe von Objekten"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Text object"
|
||||
msgstr "Text-Objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||
msgid "Barcode area"
|
||||
msgstr "QR-Code-Bereich"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||
msgid "Image area"
|
||||
msgstr "Bildbereich"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Powered by pretix"
|
||||
msgstr "Event-Ticketshop von pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Object"
|
||||
msgstr "Objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Ticket design"
|
||||
msgstr "Ticket-Design"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||
msgid "Saving failed."
|
||||
msgstr "Speichern fehlgeschlagen."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
"Möchten Sie den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
||||
@@ -540,20 +540,20 @@ msgstr "Sie erhalten %(currency)s %(amount)s zurück"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr "Bitte geben Sie den Betrag ein, den der Veranstalter einbehalten darf."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:377
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "Bitte tragen Sie eine Menge für eines der Produkte ein."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:413
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||
msgid "required"
|
||||
msgstr "verpflichtend"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:516
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:535
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||
msgid "Time zone:"
|
||||
msgstr "Zeitzone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:526
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||
msgid "Your local time:"
|
||||
msgstr "Deine lokale Zeit:"
|
||||
|
||||
@@ -651,7 +651,7 @@ msgstr "Ticket-Shop öffnen"
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr "Der Warenkorb konnte nicht erstellt werden. Bitte erneut versuchen"
|
||||
msgstr "Der Warenkorb konnte nicht erstellt werden. Bitte erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
@@ -833,84 +833,6 @@ msgstr "November"
|
||||
msgid "December"
|
||||
msgstr "Dezember"
|
||||
|
||||
#~ msgid "PayPal"
|
||||
#~ msgstr "PayPal"
|
||||
|
||||
#~ msgid "Venmo"
|
||||
#~ msgstr "Venmo"
|
||||
|
||||
#~ msgid "Apple Pay"
|
||||
#~ msgstr "Apple Pay"
|
||||
|
||||
#~ msgid "Itaú"
|
||||
#~ msgstr "Itaú"
|
||||
|
||||
#~ msgid "PayPal Credit"
|
||||
#~ msgstr "PayPal Kredit"
|
||||
|
||||
#~ msgid "Credit Card"
|
||||
#~ msgstr "Kreditkarte"
|
||||
|
||||
#~ msgid "PayPal Pay Later"
|
||||
#~ msgstr "PayPal Später Zahlen"
|
||||
|
||||
#~ msgid "iDEAL"
|
||||
#~ msgstr "iDEAL"
|
||||
|
||||
#~ msgid "SEPA Direct Debit"
|
||||
#~ msgstr "SEPA-Lastschrift"
|
||||
|
||||
#~ msgid "Bancontact"
|
||||
#~ msgstr "Bancontact"
|
||||
|
||||
#~ msgid "giropay"
|
||||
#~ msgstr "giropay"
|
||||
|
||||
#~ msgid "SOFORT"
|
||||
#~ msgstr "SOFORT"
|
||||
|
||||
#~ msgid "eps"
|
||||
#~ msgstr "eps"
|
||||
|
||||
#~ msgid "MyBank"
|
||||
#~ msgstr "MyBank"
|
||||
|
||||
#~ msgid "Przelewy24"
|
||||
#~ msgstr "Przelewy24"
|
||||
|
||||
#~ msgid "Verkkopankki"
|
||||
#~ msgstr "Verkkopankki"
|
||||
|
||||
#~ msgid "PayU"
|
||||
#~ msgstr "PayU"
|
||||
|
||||
#~ msgid "BLIK"
|
||||
#~ msgstr "BLIK"
|
||||
|
||||
#~ msgid "Trustly"
|
||||
#~ msgstr "Trustly"
|
||||
|
||||
#~ msgid "Zimpler"
|
||||
#~ msgstr "Zimpler"
|
||||
|
||||
#~ msgid "Maxima"
|
||||
#~ msgstr "Maxima"
|
||||
|
||||
#~ msgid "OXXO"
|
||||
#~ msgstr "OXXO"
|
||||
|
||||
#~ msgid "Boleto"
|
||||
#~ msgstr "Boleto"
|
||||
|
||||
#~ msgid "WeChat Pay"
|
||||
#~ msgstr "WeChat Pay"
|
||||
|
||||
#~ msgid "Mercado Pago"
|
||||
#~ msgstr "Mercado Pago"
|
||||
|
||||
#~ msgid "Payment method unavailable"
|
||||
#~ msgstr "Zahlungsmethode nicht verfügbar"
|
||||
|
||||
#~ msgid "Lead Scan QR"
|
||||
#~ msgstr "Lead-Scanning-QR-Code"
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ Alipay
|
||||
and
|
||||
App
|
||||
Apps
|
||||
APM
|
||||
as
|
||||
auschecken
|
||||
Ausgangsscan
|
||||
@@ -45,13 +44,9 @@ bez
|
||||
BezahlCode
|
||||
Bezahlmethode
|
||||
Blackberry
|
||||
BLIK
|
||||
BN
|
||||
Branding
|
||||
Browsereinstellungen
|
||||
BSD
|
||||
bspw
|
||||
Boleto
|
||||
Bundles
|
||||
bzw
|
||||
chardet
|
||||
@@ -89,7 +84,6 @@ email
|
||||
Enterprise
|
||||
EPC
|
||||
EPS
|
||||
eps
|
||||
Erstattungsbetrag
|
||||
Erstattungsliste
|
||||
Erstattungsmethode
|
||||
@@ -138,9 +132,7 @@ Installations
|
||||
integrationen
|
||||
INV
|
||||
invalidiert
|
||||
ISU
|
||||
iOS
|
||||
Itaú
|
||||
iTunes
|
||||
JavaScript
|
||||
JSON
|
||||
@@ -157,7 +149,6 @@ loszulegen
|
||||
Ltd
|
||||
max
|
||||
MapQuest
|
||||
Mercado
|
||||
Merchandise
|
||||
Meta
|
||||
Metadaten
|
||||
@@ -169,7 +160,6 @@ Mo
|
||||
MOTO
|
||||
Multibanco
|
||||
MwSt
|
||||
MyBank
|
||||
name
|
||||
Nr
|
||||
number
|
||||
@@ -179,13 +169,10 @@ OpenCage
|
||||
OpenStreetMap
|
||||
Opera
|
||||
Output
|
||||
OXXO
|
||||
parsen
|
||||
Pago
|
||||
Pay
|
||||
PayPal
|
||||
PayPals
|
||||
PayU
|
||||
PCI
|
||||
Platzhalterzeichen
|
||||
Play
|
||||
@@ -205,7 +192,6 @@ Przelewy
|
||||
pt
|
||||
px
|
||||
QR
|
||||
rabattiert
|
||||
Rabattierung
|
||||
Reader
|
||||
Rechnungs
|
||||
@@ -233,7 +219,6 @@ Scans
|
||||
Scanergebnis
|
||||
Scanning
|
||||
schiefgeht
|
||||
schiefgelaufen
|
||||
sechsstelligen
|
||||
Secret
|
||||
Security
|
||||
@@ -276,7 +261,6 @@ TOTP
|
||||
Trace
|
||||
Tracking
|
||||
transaktionale
|
||||
Trustly
|
||||
Turnover
|
||||
txt
|
||||
überbuchen
|
||||
@@ -303,7 +287,6 @@ URIs
|
||||
Ursprüngl
|
||||
USt
|
||||
Überweisungs
|
||||
Venmo
|
||||
Veranstaltereinstellungen
|
||||
Veranstalterkonten
|
||||
Veranstalterkonto
|
||||
@@ -316,7 +299,6 @@ Veranstaltungs
|
||||
veranstaltungsweiten
|
||||
Verfügbarkeitsberechnung
|
||||
Verfügbarkeitsstatus
|
||||
Verkkopankki
|
||||
Veröffentlichbarer
|
||||
VIP
|
||||
WebAuthn
|
||||
@@ -334,10 +316,8 @@ Zahlungsplugins
|
||||
Zehnerkarten
|
||||
zeitbasiert
|
||||
Zeitslotbuchung
|
||||
Zimpler
|
||||
zubuchbaren
|
||||
zurückbuchen
|
||||
zurückgeleitet
|
||||
zurückwechseln
|
||||
zzgl
|
||||
ZVT
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-05-27 13:57+0000\n"
|
||||
"PO-Revision-Date: 2022-04-28 18:04+0000\n"
|
||||
"POT-Creation-Date: 2022-04-26 16:23+0000\n"
|
||||
"PO-Revision-Date: 2021-10-25 15:40+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix-js/de_Informal/>\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.12\n"
|
||||
"X-Generator: Weblate 4.8\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -199,7 +199,7 @@ msgid "close"
|
||||
msgstr "schließen"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
@@ -208,14 +208,14 @@ msgstr ""
|
||||
"dies einige Minuten dauern."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr ""
|
||||
"Deine Anfrage befindet sich beim Server in der Warteschlange und wird bald "
|
||||
"verarbeitet."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -225,14 +225,14 @@ msgstr ""
|
||||
"verarbeitet. Wenn dies länger als zwei Minuten dauert, kontaktiere uns bitte "
|
||||
"oder gehe in deinem Browser einen Schritt zurück und versuche es erneut."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:175
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "Ein Fehler vom Typ {code} ist aufgetreten."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
@@ -240,12 +240,12 @@ msgstr ""
|
||||
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
|
||||
"Letzter Fehlercode: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:144
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
@@ -253,11 +253,11 @@ msgstr ""
|
||||
"Wir können den Server aktuell nicht erreichen. Bitte versuche es noch "
|
||||
"einmal. Fehlercode: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:208
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Wir verarbeiten deine Anfrage …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -267,7 +267,7 @@ msgstr ""
|
||||
"dauert, prüfe bitte deine Internetverbindung. Danach kannst du diese Seite "
|
||||
"neu laden und es erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:273
|
||||
#: pretix/static/pretixbase/js/asynctask.js:270
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr "Schließen"
|
||||
@@ -307,7 +307,7 @@ msgstr "Aktueller Zeitpunkt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:71
|
||||
msgid "Current day of the week (1 = Monday, 7 = Sunday)"
|
||||
msgstr "Aktueller Wochentag (1 = Montag, 7 = Sonntag)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:75
|
||||
msgid "Number of previous entries"
|
||||
@@ -323,11 +323,11 @@ msgstr "Anzahl an Tagen mit vorherigem Eintritt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:87
|
||||
msgid "Minutes since last entry (-1 on first entry)"
|
||||
msgstr "Minuten seit vorherigem Zutritt (-1 beim erstem Zutritt)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:91
|
||||
msgid "Minutes since first entry (-1 on first entry)"
|
||||
msgstr "Minuten seit erstem Zutritt (-1 bei erstem Zutritt)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:112
|
||||
msgid "All of the conditions below (AND)"
|
||||
@@ -373,49 +373,49 @@ msgstr "Minuten"
|
||||
msgid "Check-in QR"
|
||||
msgstr "Check-in-QR-Code"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "Die Hintergrund-PDF-Datei konnte nicht geladen werden:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||
msgid "Group of objects"
|
||||
msgstr "Gruppe von Objekten"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Text object"
|
||||
msgstr "Text-Objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||
msgid "Barcode area"
|
||||
msgstr "QR-Code-Bereich"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||
msgid "Image area"
|
||||
msgstr "Bildbereich"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Powered by pretix"
|
||||
msgstr "Event-Ticketshop von pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Object"
|
||||
msgstr "Objekt"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Ticket design"
|
||||
msgstr "Ticket-Design"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||
msgid "Saving failed."
|
||||
msgstr "Speichern fehlgeschlagen."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
"Es gab ein Problem beim Hochladen der PDF-Datei, bitte erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
"Möchtest du den Editor wirklich schließen ohne Ihre Änderungen zu speichern?"
|
||||
@@ -539,20 +539,20 @@ msgstr "Du erhältst %(currency)s %(amount)s zurück"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr "Bitte gib den Betrag ein, den der Veranstalter einbehalten darf."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:377
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "Bitte trage eine Menge für eines der Produkte ein."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:413
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||
msgid "required"
|
||||
msgstr "verpflichtend"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:516
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:535
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||
msgid "Time zone:"
|
||||
msgstr "Zeitzone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:526
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||
msgid "Your local time:"
|
||||
msgstr "Deine lokale Zeit:"
|
||||
|
||||
@@ -832,84 +832,6 @@ msgstr "November"
|
||||
msgid "December"
|
||||
msgstr "Dezember"
|
||||
|
||||
#~ msgid "PayPal"
|
||||
#~ msgstr "PayPal"
|
||||
|
||||
#~ msgid "Venmo"
|
||||
#~ msgstr "Venmo"
|
||||
|
||||
#~ msgid "Apple Pay"
|
||||
#~ msgstr "Apple Pay"
|
||||
|
||||
#~ msgid "Itaú"
|
||||
#~ msgstr "Itaú"
|
||||
|
||||
#~ msgid "PayPal Credit"
|
||||
#~ msgstr "PayPal Kredit"
|
||||
|
||||
#~ msgid "Credit Card"
|
||||
#~ msgstr "Kreditkarte"
|
||||
|
||||
#~ msgid "PayPal Pay Later"
|
||||
#~ msgstr "PayPal Später Zahlen"
|
||||
|
||||
#~ msgid "iDEAL"
|
||||
#~ msgstr "iDEAL"
|
||||
|
||||
#~ msgid "SEPA Direct Debit"
|
||||
#~ msgstr "SEPA-Lastschrift"
|
||||
|
||||
#~ msgid "Bancontact"
|
||||
#~ msgstr "Bancontact"
|
||||
|
||||
#~ msgid "giropay"
|
||||
#~ msgstr "giropay"
|
||||
|
||||
#~ msgid "SOFORT"
|
||||
#~ msgstr "SOFORT"
|
||||
|
||||
#~ msgid "eps"
|
||||
#~ msgstr "eps"
|
||||
|
||||
#~ msgid "MyBank"
|
||||
#~ msgstr "MyBank"
|
||||
|
||||
#~ msgid "Przelewy24"
|
||||
#~ msgstr "Przelewy24"
|
||||
|
||||
#~ msgid "Verkkopankki"
|
||||
#~ msgstr "Verkkopankki"
|
||||
|
||||
#~ msgid "PayU"
|
||||
#~ msgstr "PayU"
|
||||
|
||||
#~ msgid "BLIK"
|
||||
#~ msgstr "BLIK"
|
||||
|
||||
#~ msgid "Trustly"
|
||||
#~ msgstr "Trustly"
|
||||
|
||||
#~ msgid "Zimpler"
|
||||
#~ msgstr "Zimpler"
|
||||
|
||||
#~ msgid "Maxima"
|
||||
#~ msgstr "Maxima"
|
||||
|
||||
#~ msgid "OXXO"
|
||||
#~ msgstr "OXXO"
|
||||
|
||||
#~ msgid "Boleto"
|
||||
#~ msgstr "Boleto"
|
||||
|
||||
#~ msgid "WeChat Pay"
|
||||
#~ msgstr "WeChat Pay"
|
||||
|
||||
#~ msgid "Mercado Pago"
|
||||
#~ msgstr "Mercado Pago"
|
||||
|
||||
#~ msgid "Payment method unavailable"
|
||||
#~ msgstr "Zahlungsmethode nicht verfügbar"
|
||||
|
||||
#~ msgid "Lead Scan QR"
|
||||
#~ msgstr "Lead-Scanning-QR-Code"
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ Alipay
|
||||
and
|
||||
App
|
||||
Apps
|
||||
APM
|
||||
as
|
||||
auschecken
|
||||
Ausgangsscan
|
||||
@@ -45,13 +44,9 @@ bez
|
||||
BezahlCode
|
||||
Bezahlmethode
|
||||
Blackberry
|
||||
BLIK
|
||||
BN
|
||||
Branding
|
||||
Browsereinstellungen
|
||||
BSD
|
||||
bspw
|
||||
Boleto
|
||||
Bundles
|
||||
bzw
|
||||
chardet
|
||||
@@ -89,7 +84,6 @@ email
|
||||
Enterprise
|
||||
EPC
|
||||
EPS
|
||||
eps
|
||||
Erstattungsbetrag
|
||||
Erstattungsliste
|
||||
Erstattungsmethode
|
||||
@@ -138,9 +132,7 @@ Installations
|
||||
integrationen
|
||||
INV
|
||||
invalidiert
|
||||
ISU
|
||||
iOS
|
||||
Itaú
|
||||
iTunes
|
||||
JavaScript
|
||||
JSON
|
||||
@@ -157,7 +149,6 @@ loszulegen
|
||||
Ltd
|
||||
max
|
||||
MapQuest
|
||||
Mercado
|
||||
Merchandise
|
||||
Meta
|
||||
Metadaten
|
||||
@@ -169,7 +160,6 @@ Mo
|
||||
MOTO
|
||||
Multibanco
|
||||
MwSt
|
||||
MyBank
|
||||
name
|
||||
Nr
|
||||
number
|
||||
@@ -179,13 +169,10 @@ OpenCage
|
||||
OpenStreetMap
|
||||
Opera
|
||||
Output
|
||||
OXXO
|
||||
parsen
|
||||
Pago
|
||||
Pay
|
||||
PayPal
|
||||
PayPals
|
||||
PayU
|
||||
PCI
|
||||
Platzhalterzeichen
|
||||
Play
|
||||
@@ -205,7 +192,6 @@ Przelewy
|
||||
pt
|
||||
px
|
||||
QR
|
||||
rabattiert
|
||||
Rabattierung
|
||||
Reader
|
||||
Rechnungs
|
||||
@@ -233,7 +219,6 @@ Scans
|
||||
Scanergebnis
|
||||
Scanning
|
||||
schiefgeht
|
||||
schiefgelaufen
|
||||
sechsstelligen
|
||||
Secret
|
||||
Security
|
||||
@@ -276,7 +261,6 @@ TOTP
|
||||
Trace
|
||||
Tracking
|
||||
transaktionale
|
||||
Trustly
|
||||
Turnover
|
||||
txt
|
||||
überbuchen
|
||||
@@ -303,7 +287,6 @@ URIs
|
||||
Ursprüngl
|
||||
USt
|
||||
Überweisungs
|
||||
Venmo
|
||||
Veranstaltereinstellungen
|
||||
Veranstalterkonten
|
||||
Veranstalterkonto
|
||||
@@ -316,7 +299,6 @@ Veranstaltungs
|
||||
veranstaltungsweiten
|
||||
Verfügbarkeitsberechnung
|
||||
Verfügbarkeitsstatus
|
||||
Verkkopankki
|
||||
Veröffentlichbarer
|
||||
VIP
|
||||
WebAuthn
|
||||
@@ -334,10 +316,8 @@ Zahlungsplugins
|
||||
Zehnerkarten
|
||||
zeitbasiert
|
||||
Zeitslotbuchung
|
||||
Zimpler
|
||||
zubuchbaren
|
||||
zurückbuchen
|
||||
zurückgeleitet
|
||||
zurückwechseln
|
||||
zzgl
|
||||
ZVT
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-05-27 13:57+0000\n"
|
||||
"POT-Creation-Date: 2022-04-26 16:23+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -198,61 +198,61 @@ msgid "close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
"browser and try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:175
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:144
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:208
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
"page and try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:273
|
||||
#: pretix/static/pretixbase/js/asynctask.js:270
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr ""
|
||||
@@ -358,48 +358,48 @@ msgstr ""
|
||||
msgid "Check-in QR"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||
msgid "Group of objects"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Text object"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||
msgid "Barcode area"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||
msgid "Image area"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Powered by pretix"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Object"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Ticket design"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||
msgid "Saving failed."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
|
||||
@@ -513,20 +513,20 @@ msgstr ""
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:377
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:413
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||
msgid "required"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:516
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:535
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:526
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-05-27 13:57+0000\n"
|
||||
"POT-Creation-Date: 2022-04-26 16:23+0000\n"
|
||||
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
|
||||
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -217,7 +217,7 @@ msgid "close"
|
||||
msgstr "Κλείσιμο"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Your request has been queued on the server and will now be processed. "
|
||||
@@ -231,7 +231,7 @@ msgstr ""
|
||||
"διαρκέσει μερικά λεπτά."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Your request has been queued on the server and will now be processed. "
|
||||
@@ -243,7 +243,7 @@ msgstr ""
|
||||
"διαρκέσει μερικά λεπτά."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -253,14 +253,14 @@ msgstr ""
|
||||
"του. Αν αυτό διαρκεί περισσότερο από δύο λεπτά, επικοινωνήστε μαζί μας ή "
|
||||
"επιστρέψτε στο πρόγραμμα περιήγησής σας και δοκιμάστε ξανά."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:175
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "Παρουσιάστηκε σφάλμα τύπου {code}."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
@@ -268,14 +268,14 @@ msgstr ""
|
||||
"Αυτήν τη στιγμή δεν μπορούμε να φτάσουμε στο διακομιστή, αλλά συνεχίζουμε να "
|
||||
"προσπαθούμε. Τελευταίος κωδικός σφάλματος: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:144
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
#, fuzzy
|
||||
#| msgid "The request took to long. Please try again."
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "Το αίτημα διήρκησε πολύ. Παρακαλώ προσπαθήστε ξανά."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
@@ -283,11 +283,11 @@ msgstr ""
|
||||
"Αυτήν τη στιγμή δεν μπορούμε να συνδεθούμε με το διακομιστή. Παρακαλώ "
|
||||
"προσπαθήστε ξανά. Κωδικός σφάλματος: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:208
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Επεξεργαζόμαστε το αίτημά σας …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -297,7 +297,7 @@ msgstr ""
|
||||
"περισσότερο από ένα λεπτό, ελέγξτε τη σύνδεσή σας στο διαδίκτυο και στη "
|
||||
"συνέχεια επαναλάβετε τη φόρτωση αυτής της σελίδας και δοκιμάστε ξανά."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:273
|
||||
#: pretix/static/pretixbase/js/asynctask.js:270
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr "Κλείσιμο μηνύματος"
|
||||
@@ -406,51 +406,51 @@ msgstr ""
|
||||
msgid "Check-in QR"
|
||||
msgstr "Έλεγχος QR"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr ""
|
||||
"Το αρχείο φόντου PDF δεν ήταν δυνατό να φορτωθεί για τον ακόλουθο λόγο:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||
msgid "Group of objects"
|
||||
msgstr "Ομάδα αντικειμένων"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Text object"
|
||||
msgstr "Αντικείμενο κειμένου"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||
msgid "Barcode area"
|
||||
msgstr "Περιοχή Barcode"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||
#, fuzzy
|
||||
#| msgid "Barcode area"
|
||||
msgid "Image area"
|
||||
msgstr "Περιοχή Barcode"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Powered by pretix"
|
||||
msgstr "Υποστηρίζεται από το Pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Object"
|
||||
msgstr "Αντικείμενο"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Ticket design"
|
||||
msgstr "Σχεδιασμός εισιτηρίων"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||
msgid "Saving failed."
|
||||
msgstr "Η αποθήκευση απέτυχε."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr "Σφάλμα κατά τη μεταφόρτωση του αρχείου PDF, δοκιμάστε ξανά."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
"Θέλετε πραγματικά να αφήσετε τον επεξεργαστή χωρίς να αποθηκεύσετε τις "
|
||||
@@ -583,22 +583,22 @@ msgstr "απο %(currency)s %(price)s"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:377
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "Εισαγάγετε μια ποσότητα για έναν από τους τύπους εισιτηρίων."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:413
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "required"
|
||||
msgstr "Το καλάθι έληξε"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:516
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:535
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:526
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
@@ -880,11 +880,6 @@ msgstr "Νοέμβριος"
|
||||
msgid "December"
|
||||
msgstr "Δεκέμβριος"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Yes"
|
||||
#~ msgid "eps"
|
||||
#~ msgstr "Ναι"
|
||||
|
||||
#~ msgid "Lead Scan QR"
|
||||
#~ msgstr "Οδηγός σάρωσης QR"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-05-27 13:57+0000\n"
|
||||
"POT-Creation-Date: 2022-04-26 16:23+0000\n"
|
||||
"PO-Revision-Date: 2021-11-25 21:00+0000\n"
|
||||
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -199,7 +199,7 @@ msgid "close"
|
||||
msgstr "Cerrar"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
@@ -208,12 +208,12 @@ msgstr ""
|
||||
"dependiendo del tamaño de su evento."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr "Su solicitud ha sido enviada al servidor y será procesada en breve."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -223,14 +223,14 @@ msgstr ""
|
||||
"Si toma más de dos minutos, por favor contáctenos o regrese a la página "
|
||||
"anterior en su navegador e intente de nuevo."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:175
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "Ha ocurrido un error de tipo {code}."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
@@ -238,12 +238,12 @@ msgstr ""
|
||||
"Ahora mismo no podemos contactar con el servidor, pero lo seguimos "
|
||||
"intentando. El último código de error fue: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:144
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "La solicitud ha tomado demasiado tiempo. Por favor, intente de nuevo."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
@@ -251,11 +251,11 @@ msgstr ""
|
||||
"Ahora mismo no podemos contactar con el servidor. Por favor, intente de "
|
||||
"nuevo. Código de error: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:208
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Estamos procesando su solicitud…"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -265,7 +265,7 @@ msgstr ""
|
||||
"minuto, por favor, revise su conexión a Internet, recargue la página e "
|
||||
"intente nuevamente."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:273
|
||||
#: pretix/static/pretixbase/js/asynctask.js:270
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr "Cerrar mensaje"
|
||||
@@ -371,51 +371,51 @@ msgstr "minutos"
|
||||
msgid "Check-in QR"
|
||||
msgstr "QR de Chequeo"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr ""
|
||||
"El archivo PDF de fondo no ha podido ser cargado debido al siguiente motivo:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||
msgid "Group of objects"
|
||||
msgstr "Grupo de objetos"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Text object"
|
||||
msgstr "Objeto de texto"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||
msgid "Barcode area"
|
||||
msgstr "Área para código de barras"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||
msgid "Image area"
|
||||
msgstr "Área de imagen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Powered by pretix"
|
||||
msgstr "Proveído por pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Object"
|
||||
msgstr "Objeto"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Ticket design"
|
||||
msgstr "Diseño del ticket"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||
msgid "Saving failed."
|
||||
msgstr "El guardado falló."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
"Ha habido un error mientras se cargaba el archivo PDF, por favor, intente de "
|
||||
"nuevo."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr "¿Realmente desea salir del editor sin haber guardado sus cambios?"
|
||||
|
||||
@@ -537,20 +537,20 @@ msgstr "Obtienes %(currency)s %(price)s de vuelta"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr "Por favor, ingrese el monto que el organizador puede quedarse."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:377
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "Por favor, introduzca un valor para cada tipo de entrada."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:413
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||
msgid "required"
|
||||
msgstr "campo requerido"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:516
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:535
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||
msgid "Time zone:"
|
||||
msgstr "Zona horaria:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:526
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||
msgid "Your local time:"
|
||||
msgstr "Su hora local:"
|
||||
|
||||
@@ -831,11 +831,6 @@ msgstr "Noviembre"
|
||||
msgid "December"
|
||||
msgstr "Diciembre"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Yes"
|
||||
#~ msgid "eps"
|
||||
#~ msgstr "Sí"
|
||||
|
||||
#~ msgid "Lead Scan QR"
|
||||
#~ msgstr "Escanear QR de clientes potenciales"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-05-27 13:57+0000\n"
|
||||
"POT-Creation-Date: 2022-04-26 16:23+0000\n"
|
||||
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
|
||||
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -211,44 +211,44 @@ msgid "close"
|
||||
msgstr "Sulje"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
"browser and try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:175
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "Tapahtui virhe. Virhekoodi: {code}."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:144
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "Pyyntö aikakatkaistiin. Ole hyvä ja yritä uudelleen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
@@ -256,18 +256,18 @@ msgstr ""
|
||||
"Palvelimeen ei juuri nyt saatu yhteyttä. Ole hyvä ja yritä uudelleen. "
|
||||
"Virhekoodi: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:208
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Pyyntöäsi käsitellään …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
"page and try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:273
|
||||
#: pretix/static/pretixbase/js/asynctask.js:270
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr "Sulje viesti"
|
||||
@@ -375,50 +375,50 @@ msgstr ""
|
||||
msgid "Check-in QR"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||
msgid "Group of objects"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Text object"
|
||||
msgstr "Tekstiobjekti"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||
msgid "Barcode area"
|
||||
msgstr "Viivakoodialue"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||
#, fuzzy
|
||||
#| msgid "Barcode area"
|
||||
msgid "Image area"
|
||||
msgstr "Viivakoodialue"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Powered by pretix"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Object"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Ticket design"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||
msgid "Saving failed."
|
||||
msgstr "Tallennus epäonnistui."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
|
||||
@@ -536,22 +536,22 @@ msgstr ""
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:377
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:413
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "required"
|
||||
msgstr "Ostoskori on vanhentunut"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:516
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:535
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||
msgid "Time zone:"
|
||||
msgstr "Aikavyöhyke:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:526
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
@@ -827,11 +827,6 @@ msgstr "Marraskuu"
|
||||
msgid "December"
|
||||
msgstr "Joulukuu"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Yes"
|
||||
#~ msgid "eps"
|
||||
#~ msgstr "Kyllä"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "May"
|
||||
#~ msgid "day"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: French\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-05-27 13:57+0000\n"
|
||||
"POT-Creation-Date: 2022-04-26 16:23+0000\n"
|
||||
"PO-Revision-Date: 2022-04-07 10:40+0000\n"
|
||||
"Last-Translator: Eva-Maria Obermann <obermann@rami.io>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -214,7 +214,7 @@ msgid "close"
|
||||
msgstr "Fermer"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Your request has been queued on the server and will now be processed. "
|
||||
@@ -227,7 +227,7 @@ msgstr ""
|
||||
"taille de votre événement, cela peut prendre jusqu' à quelques minutes."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Your request has been queued on the server and will now be processed. "
|
||||
@@ -238,7 +238,7 @@ msgstr ""
|
||||
"taille de votre événement, cela peut prendre jusqu' à quelques minutes."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -248,14 +248,14 @@ msgstr ""
|
||||
"prend plus de deux minutes, veuillez nous contacter ou retourner dans votre "
|
||||
"navigateur et réessayer."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:175
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "Une erreur de type {code} s'est produite."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
@@ -263,12 +263,12 @@ msgstr ""
|
||||
"Nous ne pouvons actuellement pas atteindre le serveur, mais nous continuons "
|
||||
"d'essayer. Dernier code d'erreur: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:144
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "La requête a prit trop de temps. Veuillez réessayer."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
@@ -276,11 +276,11 @@ msgstr ""
|
||||
"Actuellement, nous ne pouvons pas atteindre le serveur. Veuillez réessayer. "
|
||||
"Code d'erreur: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:208
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Nous traitons votre demande …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -290,7 +290,7 @@ msgstr ""
|
||||
"d'une minute, veuillez vérifier votre connexion Internet, puis recharger "
|
||||
"cette page et réessayer."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:273
|
||||
#: pretix/static/pretixbase/js/asynctask.js:270
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr "Fermer le message"
|
||||
@@ -399,53 +399,53 @@ msgstr ""
|
||||
msgid "Check-in QR"
|
||||
msgstr "Enregistrement QR code"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr ""
|
||||
"Le fichier PDF généré en arrière-plan n'a pas pu être chargé pour la raison "
|
||||
"suivante :"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||
msgid "Group of objects"
|
||||
msgstr "Groupe d'objets"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Text object"
|
||||
msgstr "Objet texte"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||
msgid "Barcode area"
|
||||
msgstr "Zone de code-barres"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||
#, fuzzy
|
||||
#| msgid "Barcode area"
|
||||
msgid "Image area"
|
||||
msgstr "Zone de code-barres"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Powered by pretix"
|
||||
msgstr "Propulsé par pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Object"
|
||||
msgstr "Objet"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Ticket design"
|
||||
msgstr "Conception des billets"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||
msgid "Saving failed."
|
||||
msgstr "L'enregistrement a échoué."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
"Erreur lors du téléchargement de votre fichier PDF, veuillez réessayer."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr ""
|
||||
"Voulez-vous vraiment quitter l'éditeur sans sauvegarder vos modifications ?"
|
||||
@@ -571,22 +571,22 @@ msgstr "de %(currency)s %(price)s"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:377
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "SVP entrez une quantité pour un de vos types de billets."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:413
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "required"
|
||||
msgstr "Panier expiré"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:516
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:535
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:526
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
@@ -867,11 +867,6 @@ msgstr "Novembre"
|
||||
msgid "December"
|
||||
msgstr "Décembre"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Yes"
|
||||
#~ msgid "eps"
|
||||
#~ msgstr "Oui"
|
||||
|
||||
#~ msgid "Lead Scan QR"
|
||||
#~ msgstr "Balayage du QR code"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-05-27 13:57+0000\n"
|
||||
"POT-Creation-Date: 2022-04-26 16:23+0000\n"
|
||||
"PO-Revision-Date: 2022-02-22 22:00+0000\n"
|
||||
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
|
||||
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -199,7 +199,7 @@ msgid "close"
|
||||
msgstr "cerrar"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:43
|
||||
#: pretix/static/pretixbase/js/asynctask.js:120
|
||||
#: pretix/static/pretixbase/js/asynctask.js:119
|
||||
msgid ""
|
||||
"Your request is currently being processed. Depending on the size of your "
|
||||
"event, this might take up to a few minutes."
|
||||
@@ -208,12 +208,12 @@ msgstr ""
|
||||
"dependendo do tamaño do seu evento."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:48
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixbase/js/asynctask.js:124
|
||||
msgid "Your request has been queued on the server and will soon be processed."
|
||||
msgstr "A súa solicitude foi enviada ao servidor e será procesada en breve."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:54
|
||||
#: pretix/static/pretixbase/js/asynctask.js:131
|
||||
#: pretix/static/pretixbase/js/asynctask.js:130
|
||||
msgid ""
|
||||
"Your request arrived on the server but we still wait for it to be processed. "
|
||||
"If this takes longer than two minutes, please contact us or go back in your "
|
||||
@@ -223,14 +223,14 @@ msgstr ""
|
||||
"procesada. Se tarda máis de dous minutos, por favor, contacte con nós ou "
|
||||
"volva á páxina anterior no seu navegador e inténteo de novo."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:90
|
||||
#: pretix/static/pretixbase/js/asynctask.js:178
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixbase/js/asynctask.js:89
|
||||
#: pretix/static/pretixbase/js/asynctask.js:175
|
||||
#: pretix/static/pretixbase/js/asynctask.js:180
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:24
|
||||
msgid "An error of type {code} occurred."
|
||||
msgstr "Ocurreu un error de tipo {code}."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:93
|
||||
#: pretix/static/pretixbase/js/asynctask.js:92
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
@@ -238,12 +238,12 @@ msgstr ""
|
||||
"Agora mesmo non podemos contactar co servidor, pero seguímolo intentando. O "
|
||||
"último código de erro foi: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:145
|
||||
#: pretix/static/pretixbase/js/asynctask.js:144
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "A petición levou demasiado tempo. Inténteo de novo."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:186
|
||||
#: pretix/static/pretixbase/js/asynctask.js:183
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
@@ -251,11 +251,11 @@ msgstr ""
|
||||
"Agora mesmo non podemos contactar co servidor. Por favor, inténteo de novo. "
|
||||
"Código de erro: {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:208
|
||||
#: pretix/static/pretixbase/js/asynctask.js:205
|
||||
msgid "We are processing your request …"
|
||||
msgstr "Estamos procesando a súa solicitude…"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixbase/js/asynctask.js:213
|
||||
msgid ""
|
||||
"We are currently sending your request to the server. If this takes longer "
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
@@ -265,7 +265,7 @@ msgstr ""
|
||||
"dun minuto, por favor, revise a súa conexión a Internet, recargue a páxina e "
|
||||
"inténteo de novo."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:273
|
||||
#: pretix/static/pretixbase/js/asynctask.js:270
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:71
|
||||
msgid "Close message"
|
||||
msgstr "Cerrar mensaxe"
|
||||
@@ -371,49 +371,49 @@ msgstr "minutos"
|
||||
msgid "Check-in QR"
|
||||
msgstr "QR de validación"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:382
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:376
|
||||
msgid "The PDF background file could not be loaded for the following reason:"
|
||||
msgstr "O arquivo PDF de fondo non se puido cargar polo motivo seguinte:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:624
|
||||
msgid "Group of objects"
|
||||
msgstr "Grupo de obxectos"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:630
|
||||
msgid "Text object"
|
||||
msgstr "Obxecto de texto"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:632
|
||||
msgid "Barcode area"
|
||||
msgstr "Área para código de barras"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:640
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:634
|
||||
msgid "Image area"
|
||||
msgstr "Área de imaxe"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:636
|
||||
msgid "Powered by pretix"
|
||||
msgstr "Desenvolto por Pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:644
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:638
|
||||
msgid "Object"
|
||||
msgstr "Obxecto"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:648
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:642
|
||||
msgid "Ticket design"
|
||||
msgstr "Deseño do tícket"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:938
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:932
|
||||
msgid "Saving failed."
|
||||
msgstr "O gardado fallou."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:988
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1027
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:982
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1021
|
||||
msgid "Error while uploading your PDF file, please try again."
|
||||
msgstr ""
|
||||
"Houbo un erro mentres se cargaba o arquivo PDF. Por favor, inténteo de novo."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1012
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:1006
|
||||
msgid "Do you really want to leave the editor without saving your changes?"
|
||||
msgstr "Realmente desexa saír do editor sen gardar os cambios?"
|
||||
|
||||
@@ -534,20 +534,20 @@ msgstr "Obtés %(currency)s %(price)s de volta"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr "Por favor, ingrese a cantidade que pode conservar o organizador."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:377
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:364
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "Por favor, introduza un valor para cada tipo de entrada."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:413
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:400
|
||||
msgid "required"
|
||||
msgstr "campo requirido"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:516
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:535
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:503
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:522
|
||||
msgid "Time zone:"
|
||||
msgstr "Zona horaria:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:526
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:513
|
||||
msgid "Your local time:"
|
||||
msgstr "A súa hora local:"
|
||||
|
||||
@@ -827,10 +827,5 @@ msgstr "Novembro"
|
||||
msgid "December"
|
||||
msgstr "Decembro"
|
||||
|
||||
#, fuzzy
|
||||
#~| msgid "Yes"
|
||||
#~ msgid "eps"
|
||||
#~ msgstr "Si"
|
||||
|
||||
#~ msgid "Lead Scan QR"
|
||||
#~ msgstr "Escanear QR de clientela potencial"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user