forked from CGM_Public/pretix_original
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab4ecd0b6b | |||
| 8f13c03245 | |||
| 3b664f8b76 | |||
| 92eb5e3ece | |||
| ad38a7a407 | |||
| 51bdb274bd | |||
| 8cba60dd93 | |||
| a56c6ae1e0 | |||
| 1efd952a19 | |||
| ddd0db3d98 | |||
| dde724d0be | |||
| 9f55187690 | |||
| e572bfb752 | |||
| beccdf8dad | |||
| b141ea4ed5 | |||
| 32dd125b65 | |||
| b5794780da | |||
| 835c08f1ca | |||
| a79d5fddda | |||
| beb03e07e0 | |||
| dd2e5d09f9 | |||
| 5c2456e92e | |||
| 3579a7f298 | |||
| 5dd20de745 | |||
| 5708099fc9 | |||
| 2c2e8e7d21 | |||
| 5993482f6c | |||
| c905659dfb | |||
| 196c131ac9 | |||
| a554433fad | |||
| c552dd876c | |||
| 105ae8592d | |||
| ca4540eeb7 | |||
| 381366a248 | |||
| 816a3ec994 | |||
| 88d3d12dbc | |||
| 71caa17879 | |||
| 2ecdfde756 | |||
| 1f753a57c5 | |||
| 595c042624 | |||
| 5a5a551c21 | |||
| e74793994a | |||
| f1bdd3b7af | |||
| 13c40f9bb7 | |||
| 3e15e2a887 | |||
| 7525ee853b | |||
| f02b1be659 | |||
| 20f171b790 | |||
| 22906dfa77 | |||
| 5f74e661b3 | |||
| 5b99788354 | |||
| b45d58b60e | |||
| 2a58e958b0 | |||
| e44695dfcf | |||
| 1d289088f4 | |||
| a9be6337bc | |||
| df29d4e8c4 | |||
| bf2cabf7b6 | |||
| db614d36e6 | |||
| 13452b5d8c | |||
| 6422cd7858 | |||
| 2e87cb5691 | |||
| b53ee938bf | |||
| ec3bdd4a57 | |||
| 7b607594d8 | |||
| 53f129d5d3 | |||
| a4385c8b6e | |||
| 3acae96021 | |||
| b9add5ff6f | |||
| 4ca9813a1d | |||
| 347748896d | |||
| 0f590caa18 | |||
| 18801f2d1c | |||
| e5f29bd592 | |||
| 1f904d482b | |||
| b8ad276f53 | |||
| e109c37738 | |||
| 4d597d5be3 | |||
| ae8ec42905 | |||
| e5b89e9b08 | |||
| da91f5f117 | |||
| ae29240e58 | |||
| 74edf10b04 | |||
| e2e0eca872 | |||
| 6132e4a2c4 | |||
| 7df7d28518 | |||
| 11ab5c5eeb | |||
| 20211d2097 | |||
| d760ad38bf | |||
| 69af2cee93 | |||
| 6b199a2b9c | |||
| 94a64ba53a | |||
| 70f06a8f40 | |||
| a747ab154a | |||
| 6317233150 | |||
| 4d94158ff0 | |||
| 8f92eb2d2d | |||
| f29896b267 | |||
| 2dc625cf31 | |||
| 855226d37c | |||
| 648c0da9fe | |||
| 59e3494fa2 | |||
| c4ff57c07a | |||
| cc4fbfe4c7 | |||
| e99ee91573 | |||
| e2753686ee | |||
| 33f8b9851e | |||
| e3d8cf07af |
@@ -60,6 +60,14 @@ http {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
location /static/staticfiles.json {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
location /static/CACHE/manifest.json {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
location /static/ {
|
||||
alias /pretix/src/pretix/static.dist/;
|
||||
access_log off;
|
||||
|
||||
Vendored
+17
-14
@@ -54,6 +54,23 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sectionbox">
|
||||
<div class="icon">
|
||||
<a href="storefrontapi/index.html">
|
||||
<span class="fa fa-shopping-cart fa-fw"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="text">
|
||||
<a href="storefrontapi/index.html">
|
||||
<strong>Storefront API</strong>
|
||||
</a>
|
||||
<p>
|
||||
Documentation and reference of the headless shopping API exposed by pretix for building a custom
|
||||
storefront.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="sectionbox">
|
||||
<div class="icon">
|
||||
<a href="development/index.html">
|
||||
@@ -68,7 +85,6 @@
|
||||
pretix.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="sectionbox">
|
||||
<div class="icon">
|
||||
<a href="plugins/index.html">
|
||||
@@ -82,19 +98,6 @@
|
||||
<p>Documentation and details on plugins that ship with pretix or are officially supported.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sectionbox">
|
||||
<div class="icon">
|
||||
<a href="contents.html">
|
||||
<span class="fa fa-list fa-fw"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="text">
|
||||
<a href="contents.html">
|
||||
<strong>Table of contents</strong>
|
||||
</a>
|
||||
<p>Detailled overview of everything contained in this documentation.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<h2>Useful links</h2>
|
||||
|
||||
@@ -248,6 +248,14 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
||||
return 404;
|
||||
}
|
||||
|
||||
location /static/staticfiles.json {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
location /static/CACHE/manifest.json {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
location /static/ {
|
||||
alias /var/pretix/venv/lib/python3.11/site-packages/pretix/static.dist/;
|
||||
access_log off;
|
||||
|
||||
@@ -156,6 +156,8 @@ Field specific input errors include the name of the offending fields as keys in
|
||||
|
||||
If you see errors of type ``429 Too Many Requests``, you should read our documentation on :ref:`rest-ratelimit`.
|
||||
|
||||
.. _`rest-types`:
|
||||
|
||||
Data types
|
||||
----------
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ lines list of objects The actual invo
|
||||
├ gross_value money (string) Price including taxes
|
||||
├ tax_value money (string) Tax amount included
|
||||
├ tax_name string Name of used tax rate (e.g. "VAT")
|
||||
├ tax_code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||
└ tax_rate decimal (string) Used tax rate
|
||||
foreign_currency_display string If the invoice should also show the total and tax
|
||||
amount in a different currency, this contains the
|
||||
@@ -126,6 +127,10 @@ internal_reference string Customer's refe
|
||||
|
||||
The ``event`` attribute has been added. The organizer-level endpoint has been added.
|
||||
|
||||
.. versionchanged:: 2024.8
|
||||
|
||||
The ``tax_code`` attribute has been added.
|
||||
|
||||
|
||||
List of all invoices
|
||||
--------------------
|
||||
@@ -203,6 +208,7 @@ List of all invoices
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_name": "VAT",
|
||||
"tax_code": "S/standard",
|
||||
"tax_rate": "0.00"
|
||||
}
|
||||
],
|
||||
@@ -342,6 +348,7 @@ Fetching individual invoices
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_name": "VAT",
|
||||
"tax_code": "S/standard",
|
||||
"tax_rate": "0.00"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -84,6 +84,7 @@ fees list of objects List of fees in
|
||||
├ tax_rate decimal (string) VAT rate applied for this fee
|
||||
├ tax_value money (string) VAT included in this fee
|
||||
├ tax_rule integer The ID of the used tax rule (or ``null``)
|
||||
├ tax_code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||
└ canceled boolean Whether or not this fee has been canceled.
|
||||
downloads list of objects List of ticket download options for order-wise ticket
|
||||
downloading. This might be a multi-page PDF or a ZIP
|
||||
@@ -159,6 +160,10 @@ cancellation_date datetime Time of order c
|
||||
|
||||
The ``cancellation_date`` attribute has been added and can also be used as an ordering key.
|
||||
|
||||
.. versionchanged:: 2025.1
|
||||
|
||||
The ``tax_code`` attribute has been added.
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
Order position resource
|
||||
@@ -195,6 +200,7 @@ voucher_budget_use money (string) Amount of money
|
||||
are changed *after* the order was created. Can be ``null``.
|
||||
tax_rate decimal (string) VAT rate applied for this position
|
||||
tax_value money (string) VAT included in this position
|
||||
tax_code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||
tax_rule integer The ID of the used tax rule (or ``null``)
|
||||
secret string Secret code printed on the tickets for validation
|
||||
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
||||
@@ -255,6 +261,10 @@ pdf_data object Data object req
|
||||
|
||||
The attribute ``print_logs`` has been added.
|
||||
|
||||
.. versionchanged:: 2025.1
|
||||
|
||||
The ``tax_code`` attribute has been added.
|
||||
|
||||
.. _order-payment-resource:
|
||||
|
||||
Order payment resource
|
||||
@@ -406,6 +416,7 @@ List of all orders
|
||||
"tax_rate": "0.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_code": null,
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
@@ -645,6 +656,7 @@ Fetching individual orders
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
"tax_code": null,
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
@@ -843,7 +855,7 @@ Generating new secrets
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/regenerate_secrets/
|
||||
|
||||
Triggers generation of new ``secret`` attributes for both the order and all order positions.
|
||||
Triggers generation of new ``secret`` and ``ẁeb_secret`` attributes for both the order and all order positions.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@@ -874,7 +886,7 @@ Generating new secrets
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/regenerate_secrets/
|
||||
|
||||
Triggers generation of a new ``secret`` attribute for a single order position.
|
||||
Triggers generation of a new ``secret`` and ``web_secret`` attribute for a single order position.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@@ -1613,6 +1625,7 @@ List of all order positions
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
"tax_code": null,
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"discount": null,
|
||||
"pseudonymization_id": "MQLJvANO3B",
|
||||
@@ -1739,6 +1752,7 @@ Fetching individual positions
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
"tax_code": null,
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
|
||||
+112
-1
@@ -249,7 +249,7 @@ Endpoints
|
||||
"orderposition": null,
|
||||
"cartposition": null,
|
||||
"voucher": null
|
||||
},
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
@@ -260,3 +260,114 @@ Endpoints
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
|
||||
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/seats/bulk_block/
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/bulk_block/
|
||||
|
||||
Set the ``blocked`` attribute to ``true`` for a large number of seats at once.
|
||||
You can pass either a list of ``id`` values or a list of ``seat_guid`` values.
|
||||
You can pass up to 10,000 seats in one request.
|
||||
|
||||
The endpoint will return an error if you pass a seat ID that does not exist.
|
||||
However, it will not return an error if one of the passed seats is already blocked or sold.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_block/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ids": [12, 45, 56]
|
||||
}
|
||||
|
||||
or
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_block/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"seat_guids": ["6c0e29e5-05d6-421f-99f3-afd01478ecad", "c2899340-e2e7-4d05-8100-000a4b6d7cf4"]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param subevent_id: The ``id`` field of the subevent to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The seat could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
|
||||
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/seats/bulk_unblock/
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/bulk_unblock/
|
||||
|
||||
Set the ``blocked`` attribute to ``false`` for a large number of seats at once.
|
||||
You can pass either a list of ``id`` values or a list of ``seat_guid`` values.
|
||||
You can pass up to 10,000 seats in one request.
|
||||
|
||||
The endpoint will return an error if you pass a seat ID that does not exist.
|
||||
However, it will not return an error if one of the passed seat is already unblocked or is sold.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_unblock/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ids": [12, 45, 56]
|
||||
}
|
||||
|
||||
or
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_unblock/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"seat_guids": ["6c0e29e5-05d6-421f-99f3-afd01478ecad", "c2899340-e2e7-4d05-8100-000a4b6d7cf4"]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param subevent_id: The ``id`` field of the subevent to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The seat could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
|
||||
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
.. spelling:word-list::
|
||||
|
||||
EN16931
|
||||
DSFinV-K
|
||||
|
||||
.. _rest-taxrules:
|
||||
|
||||
Tax rules
|
||||
@@ -18,6 +23,7 @@ id integer Internal ID of
|
||||
name multi-lingual string The tax rules' name
|
||||
internal_name string An optional name that is only used in the backend
|
||||
rate decimal (string) Tax rate in percent
|
||||
code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
||||
the specified product price
|
||||
eu_reverse_charge boolean **DEPRECATED**. If ``true``, EU reverse charge rules
|
||||
@@ -42,6 +48,42 @@ custom_rules object Dynamic rules s
|
||||
|
||||
The ``custom_rules`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2023.8
|
||||
|
||||
The ``code`` attribute has been added.
|
||||
|
||||
.. _rest-taxcodes:
|
||||
|
||||
Tax codes
|
||||
---------
|
||||
|
||||
For integration with external systems, such as electronic invoicing or bookkeeping systems, the tax rate itself is often
|
||||
not sufficient information. For example, there could be many different reasons why a sale has a tax rate of 0 %, but the
|
||||
external handling of the transaction depends on which reason applies. Therefore, pretix allows to supply a codified
|
||||
reason that allows us to understand what the specific legal situation is. These tax codes are modeled after a combination
|
||||
of the code lists from the European standard EN16931 and the German standard DSFinV-K.
|
||||
|
||||
The following codes are supported:
|
||||
|
||||
- ``S/standard`` -- Standard VAT rate in the merchant country
|
||||
- ``S/reduced`` -- Reduced VAT rate in the merchant country
|
||||
- ``S/averaged`` -- Averaged VAT rate in the merchant country (known use case: agricultural businesses in Germany)
|
||||
- ``AE`` -- Reverse charge
|
||||
- ``O`` -- Services outside of scope of tax
|
||||
- ``E`` -- Exempt from tax (no reason given)
|
||||
- ``E/<reason>`` -- Exempt from tax, where ``<reason>`` is one of the codes listed in the `VATEX code list`_ version 5.0.
|
||||
- ``Z`` -- Zero-rated goods
|
||||
- ``G`` -- Free export item, VAT not charged
|
||||
- ``K`` -- VAT exempt for EEA intra-community supply of goods and services
|
||||
- ``L`` -- Canary Islands general indirect tax
|
||||
- ``M`` -- Tax for production, services and importation in Ceuta and Melilla
|
||||
- ``B`` -- Transferred (VAT), only in Italy
|
||||
|
||||
The code set in the ``code`` attribute of the tax rule is used by default. When ``eu_reverse_charge`` is active, the
|
||||
code is replaced by ``AE`` for reverse charge sales and by ``O`` for non-EU sales. When configuring custom rules, you
|
||||
should actively set a ``"code"`` key on each rule. Only for ``"action": "reverse"`` we automatically apply the code
|
||||
``AE``, in all other cases the default ``code`` of the tax rule is selected.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -74,6 +116,7 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "VAT"},
|
||||
"internal_name": "VAT",
|
||||
"code": "S/standard",
|
||||
"rate": "19.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
@@ -115,6 +158,7 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "VAT"},
|
||||
"internal_name": "VAT",
|
||||
"code": "S/standard",
|
||||
"rate": "19.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
@@ -164,6 +208,7 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "VAT"},
|
||||
"internal_name": "VAT",
|
||||
"code": "S/standard",
|
||||
"rate": "19.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
@@ -212,6 +257,7 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "VAT"},
|
||||
"internal_name": "VAT",
|
||||
"code": "S/standard",
|
||||
"rate": "20.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
@@ -258,3 +304,4 @@ Endpoints
|
||||
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use.
|
||||
|
||||
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/tax-rules-custom.schema.json
|
||||
.. _VATEX code list: https://ec.europa.eu/digital-building-blocks/sites/display/DIGITAL/Registry+of+supporting+artefacts+to+implement+EN16931#RegistryofsupportingartefactstoimplementEN16931-Codelists
|
||||
@@ -7,6 +7,7 @@ Table of contents
|
||||
user/index
|
||||
admin/index
|
||||
api/index
|
||||
storefrontapi/index
|
||||
development/index
|
||||
plugins/index
|
||||
license/faq
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
Basic concepts
|
||||
==============
|
||||
|
||||
This page describes basic concepts and definition that you need to know to interact
|
||||
with our Storefront API, such as authentication, pagination and similar definitions.
|
||||
|
||||
.. _`storefront-auth`:
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
|
||||
The storefront API requires authentication with an API key. You receive two kinds of API keys for the storefront API:
|
||||
Publishable keys and private keys. Publishable keys should be used when your website directly connects to the API.
|
||||
Private keys should be used only on server-to-server connections.
|
||||
|
||||
Localization
|
||||
------------
|
||||
|
||||
The storefront API will return localized and translated strings in many cases if you set an ``Accept-Language`` header.
|
||||
The selected locale will only be respected if it is active for the organizer or event in question.
|
||||
|
||||
.. _`storefront-compat`:
|
||||
|
||||
Compatibility
|
||||
-------------
|
||||
|
||||
.. note::
|
||||
|
||||
The storefront API is currently considered experimental and may change without notice.
|
||||
Once we declare the API stable, the following compatibility policy will apply.
|
||||
|
||||
We try to avoid any breaking changes to our API to avoid hassle on your end. If possible, we'll
|
||||
build new features in a way that keeps all pre-existing API usage unchanged. In some cases,
|
||||
this might not be possible or only possible with restrictions. In these case, any
|
||||
backwards-incompatible changes will be prominently noted in the "Changes to the REST API"
|
||||
section of our release notes. If possible, we will announce them multiple releases in advance.
|
||||
|
||||
We treat the following types of changes as *backwards-compatible* so we ask you to make sure
|
||||
that your clients can deal with them properly:
|
||||
|
||||
* Support of new API endpoints
|
||||
* Support of new HTTP methods for a given API endpoint
|
||||
* Support of new query parameters for a given API endpoint
|
||||
* New fields contained in API responses
|
||||
* New possible values of enumeration-like fields
|
||||
* Response body structure or message texts on failed requests (``4xx``, ``5xx`` response codes)
|
||||
|
||||
We treat the following types of changes as *backwards-incompatible*:
|
||||
|
||||
* Type changes of fields in API responses
|
||||
* New required input fields for an API endpoint
|
||||
* New required type for input fields of an API endpoint
|
||||
* Removal of endpoints, API methods or fields
|
||||
|
||||
Pagination
|
||||
----------
|
||||
|
||||
Most lists of objects returned by pretix' API will be paginated. The response will take
|
||||
the form of:
|
||||
|
||||
.. sourcecode:: javascript
|
||||
|
||||
{
|
||||
"count": 117,
|
||||
"next": "https://pretix.eu/api/v1/organizers/?page=2",
|
||||
"previous": null,
|
||||
"results": […],
|
||||
}
|
||||
|
||||
As you can see, the response contains the total number of results in the field ``count``.
|
||||
The fields ``next`` and ``previous`` contain links to the next and previous page of results,
|
||||
respectively, or ``null`` if there is no such page. You can use those URLs to retrieve the
|
||||
respective page.
|
||||
|
||||
The field ``results`` contains a list of objects representing the first results. For most
|
||||
objects, every page contains 50 results. You can specify a lower pagination size using the
|
||||
``page_size`` query parameter, but no more than 50.
|
||||
|
||||
Errors
|
||||
------
|
||||
|
||||
Error responses (of type 400-499) are returned in one of the following forms, depending on
|
||||
the type of error. General errors look like:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 405 Method Not Allowed
|
||||
Content-Type: application/json
|
||||
Content-Length: 42
|
||||
|
||||
{"detail": "Method 'DELETE' not allowed."}
|
||||
|
||||
Field specific input errors include the name of the offending fields as keys in the response:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{"amount": ["A valid integer is required."], "description": ["This field may not be blank."]}
|
||||
|
||||
If you see errors of type ``429 Too Many Requests``, you should read our documentation on :ref:`rest-ratelimit`.
|
||||
|
||||
Time Machine
|
||||
------------
|
||||
|
||||
Just like our shop frontend, the API allows simulating responses at a different point in time using the
|
||||
``X-Storefront-Time-Machine-Date`` header. This mechanism only works when the shop is in test mode.
|
||||
|
||||
Data types
|
||||
----------
|
||||
|
||||
See :ref:`data types <rest-types>` of the REST API.
|
||||
@@ -0,0 +1,17 @@
|
||||
.. _`storefront-api`:
|
||||
|
||||
Storefront API
|
||||
==============
|
||||
|
||||
This part of the documentation contains information about the headless e-commerce
|
||||
API exposed by pretix that can be used to build a custom checkout experience.
|
||||
|
||||
.. note::
|
||||
|
||||
The storefront API is currently considered experimental and may change without notice.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
fundamentals
|
||||
reference/index
|
||||
@@ -0,0 +1,7 @@
|
||||
API Reference
|
||||
=============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
foo
|
||||
+5
-5
@@ -32,7 +32,7 @@ dependencies = [
|
||||
"bleach==6.2.*",
|
||||
"celery==5.4.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=3.4.2",
|
||||
"cryptography>=44.0.0",
|
||||
"css-inline==0.14.*",
|
||||
"defusedcsv>=1.1.0",
|
||||
"Django[argon2]==4.2.*,>=4.2.15",
|
||||
@@ -44,7 +44,7 @@ dependencies = [
|
||||
"django-formtools==2.5.1",
|
||||
"django-hierarkey==1.2.*",
|
||||
"django-hijack==3.7.*",
|
||||
"django-i18nfield==1.9.*,>=1.9.4",
|
||||
"django-i18nfield==1.10.*",
|
||||
"django-libsass==0.9",
|
||||
"django-localflavor==4.0",
|
||||
"django-markup",
|
||||
@@ -74,7 +74,7 @@ dependencies = [
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.9.*",
|
||||
"phonenumberslite==8.13.*",
|
||||
"Pillow==11.0.*",
|
||||
"Pillow==11.1.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==5.29.*",
|
||||
"psycopg2-binary",
|
||||
@@ -97,10 +97,10 @@ dependencies = [
|
||||
"text-unidecode==1.*",
|
||||
"tlds>=2020041600",
|
||||
"tqdm==4.*",
|
||||
"ua-parser==0.18.*",
|
||||
"ua-parser==1.0.*",
|
||||
"vat_moss_forked==2020.3.20.0.11.0",
|
||||
"vobject==0.9.*",
|
||||
"webauthn==2.2.*",
|
||||
"webauthn==2.4.*",
|
||||
"zeep==4.3.*"
|
||||
]
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ from pathlib import Path
|
||||
|
||||
import setuptools
|
||||
|
||||
|
||||
sys.path.append(str(Path.cwd() / 'src'))
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ INSTALLED_APPS = [
|
||||
'pretix.presale',
|
||||
'pretix.multidomain',
|
||||
'pretix.api',
|
||||
'pretix.storefrontapi',
|
||||
'pretix.helpers',
|
||||
'rest_framework',
|
||||
'djangoformsetjs',
|
||||
|
||||
@@ -103,7 +103,7 @@ class SalesChannelMigrationMixin:
|
||||
]
|
||||
})
|
||||
|
||||
if data["sales_channels"] == all_channels:
|
||||
if set(data["sales_channels"]) == all_channels:
|
||||
data["all_sales_channels"] = True
|
||||
data["limit_sales_channels"] = []
|
||||
else:
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
@@ -43,6 +43,7 @@ from django.utils.translation import gettext as _
|
||||
from django_countries.serializers import CountryFieldMixin
|
||||
from pytz import common_timezones
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
@@ -436,7 +437,8 @@ class CloneEventSerializer(EventSerializer):
|
||||
testmode = validated_data.pop('testmode', None)
|
||||
has_subevents = validated_data.pop('has_subevents', None)
|
||||
tz = validated_data.pop('timezone', None)
|
||||
sales_channels = validated_data.pop('sales_channels', None)
|
||||
all_sales_channels = validated_data.pop('all_sales_channels', None)
|
||||
limit_sales_channels = validated_data.pop('limit_sales_channels', None)
|
||||
date_admission = validated_data.pop('date_admission', None)
|
||||
new_event = super().create({**validated_data, 'plugins': None})
|
||||
|
||||
@@ -449,8 +451,9 @@ class CloneEventSerializer(EventSerializer):
|
||||
new_event.is_public = is_public
|
||||
if testmode is not None:
|
||||
new_event.testmode = testmode
|
||||
if sales_channels is not None:
|
||||
new_event.sales_channels = sales_channels
|
||||
if all_sales_channels is not None or limit_sales_channels is not None:
|
||||
new_event.all_sales_channels = all_sales_channels
|
||||
new_event.limit_sales_channels.set(limit_sales_channels)
|
||||
if has_subevents is not None:
|
||||
new_event.has_subevents = has_subevents
|
||||
if has_subevents is not None:
|
||||
@@ -678,8 +681,8 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = TaxRule
|
||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name',
|
||||
'keep_gross_if_rate_changes', 'custom_rules')
|
||||
fields = ('id', 'name', 'rate', 'code', 'price_includes_tax', 'eu_reverse_charge', 'home_country',
|
||||
'internal_name', 'keep_gross_if_rate_changes', 'custom_rules')
|
||||
|
||||
|
||||
class EventSettingsSerializer(SettingsSerializer):
|
||||
@@ -989,6 +992,40 @@ def prefetch_by_id(items, qs, id_attr, target_attr):
|
||||
setattr(item, target_attr, result.get(getattr(item, id_attr)))
|
||||
|
||||
|
||||
class SeatBulkBlockInputSerializer(serializers.Serializer):
|
||||
ids = serializers.ListField(child=serializers.IntegerField(), required=False, allow_empty=True)
|
||||
seat_guids = serializers.ListField(child=serializers.CharField(), required=False, allow_empty=True)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = super().to_internal_value(data)
|
||||
|
||||
if data.get("seat_guids") and data.get("ids"):
|
||||
raise ValidationError("Please pass either seat_guids or ids.")
|
||||
|
||||
if data.get("seat_guids"):
|
||||
seat_ids = data["seat_guids"]
|
||||
if len(seat_ids) > 10000:
|
||||
raise ValidationError({"seat_guids": ["Please do not pass over 10000 seats."]})
|
||||
|
||||
seats = {s.seat_guid: s for s in self.context["queryset"].filter(seat_guid__in=seat_ids)}
|
||||
for s in seat_ids:
|
||||
if s not in seats:
|
||||
raise ValidationError({"seat_guids": [f"The seat '{s}' does not exist."]})
|
||||
elif data.get("ids"):
|
||||
seat_ids = data["ids"]
|
||||
if len(seat_ids) > 10000:
|
||||
raise ValidationError({"ids": ["Please do not pass over 10000 seats."]})
|
||||
|
||||
seats = self.context["queryset"].in_bulk(seat_ids)
|
||||
for s in seat_ids:
|
||||
if s not in seats:
|
||||
raise ValidationError({"ids": [f"The seat '{s}' does not exist."]})
|
||||
else:
|
||||
raise ValidationError("Please pass either seat_guids or ids.")
|
||||
|
||||
return {"seats": seats.values()}
|
||||
|
||||
|
||||
class SeatSerializer(I18nAwareModelSerializer):
|
||||
orderposition = serializers.IntegerField(source='orderposition_id')
|
||||
cartposition = serializers.IntegerField(source='cartposition_id')
|
||||
|
||||
@@ -19,57 +19,8 @@
|
||||
# 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.conf import settings
|
||||
from django.core.validators import URLValidator
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import Field
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
|
||||
|
||||
class I18nField(Field):
|
||||
def __init__(self, **kwargs):
|
||||
self.allow_blank = kwargs.pop('allow_blank', False)
|
||||
self.trim_whitespace = kwargs.pop('trim_whitespace', True)
|
||||
self.max_length = kwargs.pop('max_length', None)
|
||||
self.min_length = kwargs.pop('min_length', None)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
if hasattr(value, 'data'):
|
||||
if isinstance(value.data, dict):
|
||||
return value.data
|
||||
elif value.data is None:
|
||||
return None
|
||||
else:
|
||||
return {
|
||||
settings.LANGUAGE_CODE: str(value.data)
|
||||
}
|
||||
elif value is None:
|
||||
return None
|
||||
else:
|
||||
return {
|
||||
settings.LANGUAGE_CODE: str(value)
|
||||
}
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, str):
|
||||
return LazyI18nString(data)
|
||||
elif isinstance(data, dict):
|
||||
if any([k not in dict(settings.LANGUAGES) for k in data.keys()]):
|
||||
raise ValidationError('Invalid languages included.')
|
||||
return LazyI18nString(data)
|
||||
else:
|
||||
raise ValidationError('Invalid data type.')
|
||||
|
||||
|
||||
class I18nAwareModelSerializer(ModelSerializer):
|
||||
pass
|
||||
|
||||
|
||||
I18nAwareModelSerializer.serializer_field_mapping[I18nCharField] = I18nField
|
||||
I18nAwareModelSerializer.serializer_field_mapping[I18nTextField] = I18nField
|
||||
from i18nfield.rest_framework import I18nAwareModelSerializer, I18nField
|
||||
|
||||
|
||||
class I18nURLField(I18nField):
|
||||
@@ -84,3 +35,10 @@ class I18nURLField(I18nField):
|
||||
else:
|
||||
URLValidator()(value.data)
|
||||
return value
|
||||
|
||||
|
||||
__all__ = [
|
||||
"I18nAwareModelSerializer", # for backwards compatibility
|
||||
"I18nField", # for backwards compatibility
|
||||
"I18nURLField",
|
||||
]
|
||||
|
||||
@@ -512,11 +512,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id', 'pdf_data', 'seat',
|
||||
'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
||||
read_only_fields = (
|
||||
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
||||
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
|
||||
'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use'
|
||||
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id',
|
||||
'pdf_data', 'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -642,7 +643,8 @@ class OrderPaymentDateField(serializers.DateField):
|
||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
||||
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule',
|
||||
'tax_code', 'canceled')
|
||||
|
||||
|
||||
class PaymentURLField(serializers.URLField):
|
||||
@@ -1676,7 +1678,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = InvoiceLine
|
||||
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
|
||||
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name', 'fee_type',
|
||||
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_code', 'tax_name', 'fee_type',
|
||||
'fee_internal_type', 'event_location')
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import serializers, views, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import (
|
||||
NotFound, PermissionDenied, ValidationError,
|
||||
)
|
||||
@@ -50,8 +51,9 @@ from pretix.api.auth.permission import EventCRUDPermission
|
||||
from pretix.api.pagination import TotalOrderingFilter
|
||||
from pretix.api.serializers.event import (
|
||||
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
||||
EventSettingsSerializer, ItemMetaPropertiesSerializer, SeatSerializer,
|
||||
SubEventSerializer, TaxRuleSerializer,
|
||||
EventSettingsSerializer, ItemMetaPropertiesSerializer,
|
||||
SeatBulkBlockInputSerializer, SeatSerializer, SubEventSerializer,
|
||||
TaxRuleSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
@@ -237,9 +239,9 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
|
||||
changed = merge_dicts(enabled, disabled)
|
||||
|
||||
for module, action in changed.items():
|
||||
for module, operation in changed.items():
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.plugins.' + action,
|
||||
'pretix.event.plugins.' + operation,
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'plugin': module}
|
||||
@@ -744,3 +746,24 @@ class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
auth=self.request.auth,
|
||||
data={"seats": [serializer.instance.pk]},
|
||||
)
|
||||
|
||||
def bulk_change_blocked(self, blocked):
|
||||
s = SeatBulkBlockInputSerializer(
|
||||
data=self.request.data,
|
||||
context={"event": self.request.event, "queryset": self.get_queryset()},
|
||||
)
|
||||
s.is_valid(raise_exception=True)
|
||||
|
||||
seats = s.validated_data["seats"]
|
||||
for seat in seats:
|
||||
seat.blocked = blocked
|
||||
Seat.objects.bulk_update(seats, ["blocked"], batch_size=1000)
|
||||
return Response({})
|
||||
|
||||
@action(methods=["POST"], detail=False)
|
||||
def bulk_block(self, request, *args, **kwargs):
|
||||
return self.bulk_change_blocked(True)
|
||||
|
||||
@action(methods=["POST"], detail=False)
|
||||
def bulk_unblock(self, request, *args, **kwargs):
|
||||
return self.bulk_change_blocked(False)
|
||||
|
||||
@@ -35,6 +35,7 @@ from django.db.models import (
|
||||
from django.db.models.functions import Coalesce, Concat
|
||||
from django.http import FileResponse, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import formats
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import gettext as _
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
@@ -67,6 +68,7 @@ from pretix.api.serializers.orderchange import (
|
||||
OrderPositionInfoPatchSerializer,
|
||||
)
|
||||
from pretix.api.views import RichOrderingFilter
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
|
||||
@@ -97,7 +99,6 @@ from pretix.base.services.tickets import generate
|
||||
from pretix.base.signals import (
|
||||
order_modified, order_paid, order_placed, register_ticket_outputs,
|
||||
)
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.control.signals import order_search_filter_q
|
||||
from pretix.helpers import OF_SELF
|
||||
|
||||
@@ -646,6 +647,8 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
order = self.get_object()
|
||||
order.secret = generate_secret()
|
||||
for op in order.all_positions.all():
|
||||
op.web_secret = generate_secret()
|
||||
op.save(update_fields=["web_secret"])
|
||||
assign_ticket_secret(
|
||||
request.event, op, force_invalidate=True, save=True
|
||||
)
|
||||
@@ -1228,9 +1231,10 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
||||
price = get_price(**kwargs)
|
||||
tr = kwargs.get('tax_rule', kwargs.get('item').tax_rule)
|
||||
with language(data.get('locale') or self.request.event.settings.locale, self.request.event.settings.region):
|
||||
gross_formatted = formats.localize_input(round_decimal(price.gross, self.request.event.currency))
|
||||
return Response({
|
||||
'gross': price.gross,
|
||||
'gross_formatted': money_filter(price.gross, self.request.event.currency, hide_currency=True),
|
||||
'gross_formatted': gross_formatted,
|
||||
'net': price.net,
|
||||
'rate': price.rate,
|
||||
'name': str(price.name),
|
||||
|
||||
@@ -35,6 +35,7 @@ from django.utils.translation import get_language, gettext_lazy as _
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.signals import register_html_mail_renderers
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
|
||||
from pretix.base.services.placeholders import ( # noqa
|
||||
get_available_placeholders, PlaceholderContext
|
||||
@@ -79,7 +80,7 @@ class BaseHTMLMailRenderer:
|
||||
return self.identifier
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
|
||||
position=None) -> str:
|
||||
position=None, context=None) -> str:
|
||||
"""
|
||||
This method should generate the HTML part of the email.
|
||||
|
||||
@@ -88,6 +89,7 @@ class BaseHTMLMailRenderer:
|
||||
:param subject: The email subject.
|
||||
:param order: The order if this email is connected to one, otherwise ``None``.
|
||||
:param position: The order position if this email is connected to one, otherwise ``None``.
|
||||
:param context: Context to use to render placeholders in the plain body
|
||||
:return: An HTML string
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
@@ -134,8 +136,10 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
def compile_markdown(self, plaintext):
|
||||
return markdown_compile_email(plaintext)
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
|
||||
body_md = self.compile_markdown(plain_body)
|
||||
if context:
|
||||
body_md = format_map(body_md, context=context, mode=SafeFormatter.MODE_RICH_TO_HTML)
|
||||
htmlctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
|
||||
@@ -100,7 +100,7 @@ class MarkdownTextarea(forms.Textarea):
|
||||
|
||||
|
||||
class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
|
||||
def format_output(self, rendered_widgets) -> str:
|
||||
def format_output(self, rendered_widgets, id_) -> str:
|
||||
rendered_widgets = rendered_widgets + [
|
||||
'<div class="i18n-field-markdown-note">%s</div>' % (
|
||||
_("You can use {markup_name} in this field.").format(
|
||||
@@ -108,11 +108,11 @@ class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
|
||||
)
|
||||
)
|
||||
]
|
||||
return super().format_output(rendered_widgets)
|
||||
return super().format_output(rendered_widgets, id_)
|
||||
|
||||
|
||||
class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
|
||||
def format_output(self, rendered_widgets) -> str:
|
||||
def format_output(self, rendered_widgets, id_) -> str:
|
||||
rendered_widgets = rendered_widgets + [
|
||||
'<div class="i18n-field-markdown-note">%s</div>' % (
|
||||
_("You can use {markup_name} in this field.").format(
|
||||
@@ -120,7 +120,7 @@ class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
|
||||
)
|
||||
)
|
||||
]
|
||||
return super().format_output(rendered_widgets)
|
||||
return super().format_output(rendered_widgets, id_)
|
||||
|
||||
|
||||
SECRET_REDACTED = '*****'
|
||||
|
||||
@@ -277,6 +277,10 @@ class NamePartsFormField(forms.MultiValueField):
|
||||
return value
|
||||
|
||||
|
||||
def name_parts_is_empty(name_parts_dict):
|
||||
return not any(k != "_scheme" and v for k, v in name_parts_dict.items())
|
||||
|
||||
|
||||
class WrappedPhonePrefixSelect(Select):
|
||||
initial = None
|
||||
|
||||
@@ -1031,6 +1035,18 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
'is_business': ''
|
||||
}
|
||||
|
||||
@property
|
||||
def ask_vat_id(self):
|
||||
return self.event.settings.invoice_address_vatid
|
||||
|
||||
@property
|
||||
def address_required(self):
|
||||
return self.event.settings.invoice_address_required
|
||||
|
||||
@property
|
||||
def company_required(self):
|
||||
return self.event.settings.invoice_address_company_required
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = event = kwargs.pop('event')
|
||||
self.request = kwargs.pop('request', None)
|
||||
@@ -1042,7 +1058,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
if not event.settings.invoice_address_vatid:
|
||||
|
||||
self.fields["company"].widget.attrs["data-display-dependency"] = f'#id_{self.add_prefix("is_business")}_1'
|
||||
self.fields["vat_id"].widget.attrs["data-display-dependency"] = f'#id_{self.add_prefix("is_business")}_1'
|
||||
|
||||
if not self.ask_vat_id:
|
||||
del self.fields['vat_id']
|
||||
elif self.validate_vat_id:
|
||||
self.fields['vat_id'].help_text = '<br/>'.join([
|
||||
@@ -1096,13 +1116,13 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
self.data = self.data.copy()
|
||||
del self.data[fprefix + 'vat_id']
|
||||
|
||||
if not event.settings.invoice_address_required or self.all_optional:
|
||||
if not self.address_required or self.all_optional:
|
||||
for k, f in self.fields.items():
|
||||
f.required = False
|
||||
f.widget.is_required = False
|
||||
if 'required' in f.widget.attrs:
|
||||
del f.widget.attrs['required']
|
||||
elif event.settings.invoice_address_company_required and not self.all_optional:
|
||||
elif self.company_required and not self.all_optional:
|
||||
self.initial['is_business'] = True
|
||||
|
||||
self.fields['is_business'].widget = BusinessBooleanRadio(require_business=True)
|
||||
@@ -1119,11 +1139,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
label=_('Name'),
|
||||
initial=self.instance.name_parts,
|
||||
)
|
||||
if event.settings.invoice_address_required and not event.settings.invoice_address_company_required and not self.all_optional:
|
||||
if self.address_required and not self.company_required and not self.all_optional:
|
||||
if not event.settings.invoice_name_required:
|
||||
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
|
||||
self.fields['name_parts'].widget.attrs['data-required-if'] = f'#id_{self.add_prefix("is_business")}_0'
|
||||
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
|
||||
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
|
||||
self.fields['company'].widget.attrs['data-required-if'] = f'#id_{self.add_prefix("is_business")}_1'
|
||||
|
||||
if not event.settings.invoice_address_beneficiary:
|
||||
del self.fields['beneficiary']
|
||||
@@ -1149,12 +1169,12 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
data['vat_id'] = ''
|
||||
if data.get('is_business') and not ask_for_vat_id(data.get('country')):
|
||||
data['vat_id'] = ''
|
||||
if self.event.settings.invoice_address_required:
|
||||
if self.address_validation and self.address_required and not self.all_optional:
|
||||
if data.get('is_business') and not data.get('company'):
|
||||
raise ValidationError({"company": _('You need to provide a company name.')})
|
||||
if not data.get('is_business') and not data.get('name_parts'):
|
||||
if not data.get('is_business') and name_parts_is_empty(data.get('name_parts', {})):
|
||||
raise ValidationError(_('You need to provide your name.'))
|
||||
if not self.all_optional and 'street' in self.fields and not data.get('street') and not data.get('zipcode') and not data.get('city'):
|
||||
if not data.get('street') and not data.get('zipcode') and not data.get('city'):
|
||||
raise ValidationError({"street": _('This field is required.')})
|
||||
|
||||
if 'vat_id' in self.changed_data or not data.get('vat_id'):
|
||||
@@ -1167,7 +1187,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
|
||||
if all(
|
||||
not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts')
|
||||
) and len(data.get('name_parts', {})) == 1:
|
||||
) and name_parts_is_empty(data.get('name_parts', {})):
|
||||
# Do not save the country if it is the only field set -- we don't know the user even checked it!
|
||||
self.cleaned_data['country'] = ''
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class Command(BaseCommand):
|
||||
try:
|
||||
r = receiver(signal=periodic_task, sender=self)
|
||||
except Exception as err:
|
||||
if isinstance(Exception, KeyboardInterrupt):
|
||||
if isinstance(err, KeyboardInterrupt):
|
||||
raise err
|
||||
if settings.SENTRY_ENABLED:
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Generated by Django 4.2.8 on 2024-07-02 10:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"pretixbase",
|
||||
"0273_remove_checkinlist_auto_checkin_sales_channels",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="invoiceline",
|
||||
name="tax_code",
|
||||
field=models.CharField(max_length=190, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="orderfee",
|
||||
name="tax_code",
|
||||
field=models.CharField(max_length=190, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="orderposition",
|
||||
name="tax_code",
|
||||
field=models.CharField(max_length=190, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="taxrule",
|
||||
name="code",
|
||||
field=models.CharField(max_length=190, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="transaction",
|
||||
name="tax_code",
|
||||
field=models.CharField(max_length=190, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,62 @@
|
||||
# Generated by Django 4.2.17 on 2025-01-01 20:25
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0274_tax_codes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="CheckoutSession",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("cart_id", models.CharField(max_length=255, unique=True)),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("testmode", models.BooleanField(default=False)),
|
||||
("session_data", models.JSONField(default=dict)),
|
||||
(
|
||||
"customer",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="checkout_sessions",
|
||||
to="pretixbase.customer",
|
||||
),
|
||||
),
|
||||
(
|
||||
"event",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="pretixbase.event",
|
||||
),
|
||||
),
|
||||
(
|
||||
"sales_channel",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="pretixbase.saleschannel",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="invoiceaddress",
|
||||
name="checkout_session",
|
||||
field=models.OneToOneField(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="invoice_address",
|
||||
to="pretixbase.checkoutsession",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -441,6 +441,7 @@ class Price(DecimalColumnMixin, ImportColumn):
|
||||
position.price = p.gross
|
||||
position.tax_rule = position.item.tax_rule
|
||||
position.tax_rate = p.rate
|
||||
position.tax_code = p.code
|
||||
position.tax_value = p.tax
|
||||
|
||||
|
||||
@@ -584,7 +585,7 @@ class SeatColumn(ImportColumn):
|
||||
raise ValidationError(_('Multiple matching seats were found.'))
|
||||
except Seat.DoesNotExist:
|
||||
raise ValidationError(_('No matching seat was found.'))
|
||||
if not value.is_available() or value in self._cached:
|
||||
if not value.is_available(sales_channel=previous_values.get('sales_channel')) or value in self._cached:
|
||||
raise ValidationError(
|
||||
_('The seat you selected has already been taken. Please select a different seat.'))
|
||||
self._cached.add(value)
|
||||
@@ -753,11 +754,11 @@ def get_order_import_columns(event):
|
||||
AttendeeState(event),
|
||||
Price(event),
|
||||
Secret(event),
|
||||
Saleschannel(event),
|
||||
SeatColumn(event),
|
||||
ValidFrom(event),
|
||||
ValidUntil(event),
|
||||
Locale(event),
|
||||
Saleschannel(event),
|
||||
CheckinAttentionColumn(event),
|
||||
CheckinTextColumn(event),
|
||||
Expires(event),
|
||||
|
||||
@@ -823,6 +823,9 @@ class Event(EventMixin, LoggedModel):
|
||||
self.save()
|
||||
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
||||
|
||||
if hasattr(other, 'alternative_domain_assignment'):
|
||||
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
|
||||
|
||||
if not self.all_sales_channels:
|
||||
self.limit_sales_channels.set(
|
||||
self.organizer.sales_channels.filter(
|
||||
|
||||
@@ -362,6 +362,7 @@ class InvoiceLine(models.Model):
|
||||
tax_value = models.DecimalField(max_digits=13, decimal_places=2, default=Decimal('0.00'))
|
||||
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
|
||||
tax_name = models.CharField(max_length=190)
|
||||
tax_code = models.CharField(max_length=190, null=True, blank=True)
|
||||
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
||||
event_date_from = models.DateTimeField(null=True)
|
||||
event_date_to = models.DateTimeField(null=True)
|
||||
|
||||
@@ -837,7 +837,7 @@ class Item(LoggedModel):
|
||||
|
||||
if not self.tax_rule:
|
||||
t = TaxedPrice(gross=price - bundled_sum, net=price - bundled_sum, tax=Decimal('0.00'),
|
||||
rate=Decimal('0.00'), name='')
|
||||
rate=Decimal('0.00'), name='', code=None)
|
||||
else:
|
||||
t = self.tax_rule.tax(price, base_price_is=base_price_is, invoice_address=invoice_address,
|
||||
override_tax_rate=override_tax_rate, currency=currency or self.event.currency,
|
||||
@@ -845,6 +845,7 @@ class Item(LoggedModel):
|
||||
|
||||
if bundled_sum:
|
||||
t.name = "MIXED!"
|
||||
t.code = None
|
||||
t.gross += bundled_sum
|
||||
t.net += bundled_sum_net
|
||||
t.tax += bundled_sum_tax
|
||||
@@ -1258,7 +1259,7 @@ class ItemVariation(models.Model):
|
||||
|
||||
if not self.item.tax_rule:
|
||||
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
||||
rate=Decimal('0.00'), name='')
|
||||
rate=Decimal('0.00'), name='', code=None)
|
||||
else:
|
||||
t = self.item.tax_rule.tax(price, base_price_is=base_price_is, currency=currency,
|
||||
override_tax_rate=override_tax_rate,
|
||||
@@ -1280,6 +1281,7 @@ class ItemVariation(models.Model):
|
||||
t.net += bprice.net - compare_price.net
|
||||
t.tax += bprice.tax - compare_price.tax
|
||||
t.name = "MIXED!"
|
||||
t.code = None
|
||||
|
||||
return t
|
||||
|
||||
|
||||
@@ -55,16 +55,17 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, transaction
|
||||
from django.db.models import (
|
||||
Case, Exists, F, Max, OuterRef, Q, Subquery, Sum, Value, When,
|
||||
Case, Exists, F, Max, OuterRef, Prefetch, Q, Subquery, Sum, Value, When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string, salted_hmac
|
||||
from django.utils.encoding import escape_uri_path
|
||||
from django.utils.encoding import escape_uri_path, force_str
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.hashable import make_hashable
|
||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_countries.fields import Country
|
||||
@@ -1256,7 +1257,7 @@ class Order(LockModel, LoggedModel):
|
||||
keys = set(target_transaction_count.keys()) | set(current_transaction_count.keys())
|
||||
create = []
|
||||
for k in keys:
|
||||
positionid, itemid, variationid, subeventid, price, taxrate, taxruleid, taxvalue, feetype, internaltype = k
|
||||
positionid, itemid, variationid, subeventid, price, taxrate, taxruleid, taxvalue, feetype, internaltype, taxcode = k
|
||||
d = target_transaction_count[k] - current_transaction_count[k]
|
||||
if d:
|
||||
create.append(Transaction(
|
||||
@@ -1272,6 +1273,7 @@ class Order(LockModel, LoggedModel):
|
||||
tax_rate=taxrate,
|
||||
tax_rule_id=taxruleid,
|
||||
tax_value=taxvalue,
|
||||
tax_code=taxcode,
|
||||
fee_type=feetype,
|
||||
internal_type=internaltype,
|
||||
))
|
||||
@@ -2275,6 +2277,7 @@ class OrderFee(models.Model):
|
||||
FEE_TYPE_SERVICE = "service"
|
||||
FEE_TYPE_CANCELLATION = "cancellation"
|
||||
FEE_TYPE_INSURANCE = "insurance"
|
||||
FEE_TYPE_LATE = "late"
|
||||
FEE_TYPE_OTHER = "other"
|
||||
FEE_TYPE_GIFTCARD = "giftcard"
|
||||
FEE_TYPES = (
|
||||
@@ -2283,6 +2286,7 @@ class OrderFee(models.Model):
|
||||
(FEE_TYPE_SERVICE, _("Service fee")),
|
||||
(FEE_TYPE_CANCELLATION, _("Cancellation fee")),
|
||||
(FEE_TYPE_INSURANCE, _("Insurance fee")),
|
||||
(FEE_TYPE_LATE, _("Late fee")),
|
||||
(FEE_TYPE_OTHER, _("Other fees")),
|
||||
(FEE_TYPE_GIFTCARD, _("Gift card")),
|
||||
)
|
||||
@@ -2311,6 +2315,10 @@ class OrderFee(models.Model):
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True
|
||||
)
|
||||
tax_code = models.CharField(
|
||||
max_length=190,
|
||||
null=True, blank=True,
|
||||
)
|
||||
tax_value = models.DecimalField(
|
||||
max_digits=13, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
@@ -2338,6 +2346,16 @@ class OrderFee(models.Model):
|
||||
self._transaction_key_reset()
|
||||
return super().refresh_from_db(using, fields)
|
||||
|
||||
def get_tax_code_display(self):
|
||||
from pretix.base.models.tax import get_tax_code_labels
|
||||
|
||||
if self.tax_code:
|
||||
choices_dict = get_tax_code_labels()
|
||||
return force_str(
|
||||
choices_dict.get(make_hashable(self.tax_code), self.tax_code), strings_only=True
|
||||
)
|
||||
return ""
|
||||
|
||||
def _transaction_key_reset(self):
|
||||
self.__initial_transaction_key = Transaction.key(self)
|
||||
self.__initial_canceled = self.canceled
|
||||
@@ -2368,9 +2386,11 @@ class OrderFee(models.Model):
|
||||
if self.tax_rule:
|
||||
tax = self.tax_rule.tax(self.value, base_price_is='gross', invoice_address=ia, force_fixed_gross_price=True)
|
||||
self.tax_rate = tax.rate
|
||||
self.tax_code = tax.code
|
||||
self.tax_value = tax.tax
|
||||
else:
|
||||
self.tax_value = Decimal('0.00')
|
||||
self.tax_code = None
|
||||
self.tax_rate = Decimal('0.00')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@@ -2379,6 +2399,7 @@ class OrderFee(models.Model):
|
||||
|
||||
if self.tax_rate is None:
|
||||
self._calculate_tax()
|
||||
|
||||
self.order.touch()
|
||||
|
||||
if not self.get_deferred_fields():
|
||||
@@ -2466,6 +2487,10 @@ class OrderPosition(AbstractPosition):
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True
|
||||
)
|
||||
tax_code = models.CharField(
|
||||
max_length=190,
|
||||
null=True, blank=True,
|
||||
)
|
||||
tax_value = models.DecimalField(
|
||||
max_digits=13, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
@@ -2523,6 +2548,16 @@ class OrderPosition(AbstractPosition):
|
||||
models.UniqueConstraint("organizer", "secret", name="orderposition_organizer_secret_uniq")
|
||||
]
|
||||
|
||||
def get_tax_code_display(self):
|
||||
from pretix.base.models.tax import get_tax_code_labels
|
||||
|
||||
if self.tax_code:
|
||||
choices_dict = get_tax_code_labels()
|
||||
return force_str(
|
||||
choices_dict.get(make_hashable(self.tax_code), self.tax_code), strings_only=True
|
||||
)
|
||||
return ""
|
||||
|
||||
@cached_property
|
||||
def sort_key(self):
|
||||
return self.addon_to.positionid if self.addon_to else self.positionid, self.addon_to_id or 0, self.positionid
|
||||
@@ -2695,11 +2730,13 @@ class OrderPosition(AbstractPosition):
|
||||
if self.tax_rule:
|
||||
tax = self.tax_rule.tax(self.price, invoice_address=ia, base_price_is='gross', force_fixed_gross_price=True)
|
||||
self.tax_rate = tax.rate
|
||||
self.tax_code = tax.code
|
||||
self.tax_value = tax.tax
|
||||
if tax.gross != self.price:
|
||||
raise ValueError('Invalid tax calculation')
|
||||
else:
|
||||
self.tax_value = Decimal('0.00')
|
||||
self.tax_code = None
|
||||
self.tax_rate = Decimal('0.00')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@@ -2970,6 +3007,10 @@ class Transaction(models.Model):
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True
|
||||
)
|
||||
tax_code = models.CharField(
|
||||
max_length=190,
|
||||
null=True, blank=True,
|
||||
)
|
||||
tax_value = models.DecimalField(
|
||||
max_digits=13, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
@@ -2990,17 +3031,27 @@ class Transaction(models.Model):
|
||||
raise ValidationError('Should set either item or fee type')
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_tax_code_display(self):
|
||||
from pretix.base.models.tax import get_tax_code_labels
|
||||
|
||||
if self.tax_code:
|
||||
choices_dict = get_tax_code_labels()
|
||||
return force_str(
|
||||
choices_dict.get(make_hashable(self.tax_code), self.tax_code), strings_only=True
|
||||
)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def key(obj):
|
||||
if isinstance(obj, Transaction):
|
||||
return (obj.positionid, obj.item_id, obj.variation_id, obj.subevent_id, obj.price, obj.tax_rate,
|
||||
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type)
|
||||
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type, obj.tax_code)
|
||||
elif isinstance(obj, OrderPosition):
|
||||
return (obj.positionid, obj.item_id, obj.variation_id, obj.subevent_id, obj.price, obj.tax_rate,
|
||||
obj.tax_rule_id, obj.tax_value, None, None)
|
||||
obj.tax_rule_id, obj.tax_value, None, None, obj.tax_code)
|
||||
elif isinstance(obj, OrderFee):
|
||||
return (None, None, None, None, obj.value, obj.tax_rate,
|
||||
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type)
|
||||
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type, obj.tax_code)
|
||||
raise ValueError('invalid state') # noqa
|
||||
|
||||
@property
|
||||
@@ -3012,6 +3063,64 @@ class Transaction(models.Model):
|
||||
return self.tax_value * self.count
|
||||
|
||||
|
||||
class CheckoutSession(models.Model):
|
||||
"""
|
||||
A checkout session optionally bundles cart positions with additional information. This is historically
|
||||
not required in pretix and currently only used in the Storefront API.
|
||||
"""
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
verbose_name=_("Event"),
|
||||
related_name="checkout_sessions",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
cart_id = models.CharField(
|
||||
max_length=255, unique=True,
|
||||
verbose_name=_("Cart ID (e.g. session key)"),
|
||||
)
|
||||
created = models.DateTimeField(
|
||||
verbose_name=_("Date"),
|
||||
auto_now_add=True,
|
||||
)
|
||||
customer = models.ForeignKey(
|
||||
Customer,
|
||||
related_name='checkout_sessions',
|
||||
null=True, blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
sales_channel = models.ForeignKey(
|
||||
"SalesChannel",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
testmode = models.BooleanField(default=False)
|
||||
session_data = models.JSONField(default=dict)
|
||||
|
||||
def get_cart_positions(self, prefetch_questions=False):
|
||||
qs = CartPosition.objects.filter(event=self.event, cart_id=self.cart_id).select_related(
|
||||
"item", "variation", "subevent",
|
||||
)
|
||||
if prefetch_questions:
|
||||
qqs = self.event.questions.filter(ask_during_checkin=False, hidden=False)
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch("answers",
|
||||
QuestionAnswer.objects.prefetch_related("options"),
|
||||
to_attr="answerlist"),
|
||||
Prefetch("item__questions",
|
||||
qqs.prefetch_related(
|
||||
Prefetch("options", QuestionOption.objects.prefetch_related(Prefetch(
|
||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||
# a prefetch lookup on this query...
|
||||
"question",
|
||||
Question.objects.none(),
|
||||
to_attr="dummy"
|
||||
)))
|
||||
).select_related("dependency_question"),
|
||||
to_attr="questions_to_ask")
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class CartPosition(AbstractPosition):
|
||||
"""
|
||||
A cart position is similar to an order line, except that it is not
|
||||
@@ -3164,6 +3273,7 @@ class CartPosition(AbstractPosition):
|
||||
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.tax_code = line_price.code
|
||||
self.save(update_fields=['line_price_gross', 'tax_rate'])
|
||||
|
||||
@property
|
||||
@@ -3193,6 +3303,13 @@ class CartPosition(AbstractPosition):
|
||||
|
||||
class InvoiceAddress(models.Model):
|
||||
last_modified = models.DateTimeField(auto_now=True)
|
||||
checkout_session = models.OneToOneField(
|
||||
CheckoutSession,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='invoice_address',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE)
|
||||
customer = models.ForeignKey(
|
||||
Customer,
|
||||
|
||||
+220
-12
@@ -21,6 +21,7 @@
|
||||
#
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
import jsonschema
|
||||
from django.contrib.staticfiles import finders
|
||||
@@ -30,8 +31,9 @@ from django.db import models
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.formats import localize
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.hashable import make_hashable
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _, pgettext
|
||||
from django.utils.translation import gettext_lazy as _, pgettext, pgettext_lazy
|
||||
from i18nfield.fields import I18nCharField
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
@@ -42,7 +44,7 @@ from pretix.helpers.countries import FastCountryField
|
||||
|
||||
|
||||
class TaxedPrice:
|
||||
def __init__(self, *, gross: Decimal, net: Decimal, tax: Decimal, rate: Decimal, name: str):
|
||||
def __init__(self, *, gross: Decimal, net: Decimal, tax: Decimal, rate: Decimal, name: str, code: Optional[str]):
|
||||
if net + tax != gross:
|
||||
raise ValueError('Net value and tax value need to add to the gross value')
|
||||
self.gross = gross
|
||||
@@ -50,6 +52,7 @@ class TaxedPrice:
|
||||
self.tax = tax
|
||||
self.rate = rate
|
||||
self.name = name
|
||||
self.code = code
|
||||
|
||||
def __repr__(self):
|
||||
return '{} + {}% = {}'.format(localize(self.net), localize(self.rate), localize(self.gross))
|
||||
@@ -72,6 +75,7 @@ class TaxedPrice:
|
||||
tax=newgross - newnet,
|
||||
rate=self.rate,
|
||||
name=self.name,
|
||||
code=self.code,
|
||||
)
|
||||
|
||||
def __mul__(self, other):
|
||||
@@ -85,6 +89,7 @@ class TaxedPrice:
|
||||
tax=newgross - newnet,
|
||||
rate=self.rate,
|
||||
name=self.name,
|
||||
code=self.code,
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
@@ -93,7 +98,8 @@ class TaxedPrice:
|
||||
self.net == other.net and
|
||||
self.tax == other.tax and
|
||||
self.rate == other.rate and
|
||||
self.name == other.name
|
||||
self.name == other.name and
|
||||
self.code == other.code
|
||||
)
|
||||
|
||||
|
||||
@@ -102,7 +108,8 @@ TAXED_ZERO = TaxedPrice(
|
||||
net=Decimal('0.00'),
|
||||
tax=Decimal('0.00'),
|
||||
rate=Decimal('0.00'),
|
||||
name=''
|
||||
name='',
|
||||
code=None,
|
||||
)
|
||||
|
||||
EU_COUNTRIES = {
|
||||
@@ -125,6 +132,152 @@ VAT_ID_COUNTRIES = EU_COUNTRIES | {'CH', 'NO'}
|
||||
format_html_lazy = lazy(format_html, str)
|
||||
|
||||
|
||||
TAX_CODE_LISTS = (
|
||||
# Sources:
|
||||
# https://ec.europa.eu/digital-building-blocks/sites/display/DIGITAL/Registry+of+supporting+artefacts+to+implement+EN16931#RegistryofsupportingartefactstoimplementEN16931-Codelists#RegistryofsupportingartefactstoimplementEN16931-Codelists
|
||||
# https://docs.peppol.eu/poacc/billing/3.0/codelist/vatex/
|
||||
# https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL5305/
|
||||
# https://www.bzst.de/DE/Unternehmen/Aussenpruefungen/DigitaleSchnittstelleFinV/digitaleschnittstellefinv_node.html#js-toc-entry2
|
||||
#
|
||||
# !! When changed, also update tax-rules-custom.schema.json and doc/api/resources/taxrules.rst !!
|
||||
(
|
||||
_("Standard rates"),
|
||||
(
|
||||
# Standard rate in any country, such as 19% in Germany or 20% in Austria
|
||||
# DSFinV-K mapping: 1
|
||||
("S/standard", pgettext_lazy("tax_code", "Standard rate")),
|
||||
|
||||
# Reduced rate in any country, such as 7% in Germany or both 10% and 13% in Austria
|
||||
# DSFinV-K mapping: 2
|
||||
("S/reduced", pgettext_lazy("tax_code", "Reduced rate")),
|
||||
|
||||
# Averaged rate, for example Germany § 24 (1) Nr. 3 UStG "für die übrigen Umsätze" in agricultural and silvicultural businesses
|
||||
# DSFinV-K mapping: 3
|
||||
("S/averaged", pgettext_lazy("tax_code", "Averaged rate (other revenue in a agricultural and silvicultural business)")),
|
||||
|
||||
# We ignore the German special case of the actual silvicultural products as they won't be sold through pretix (DSFinV-K mapping: 4)
|
||||
)
|
||||
),
|
||||
(
|
||||
_("Reverse charge"),
|
||||
(
|
||||
("AE", pgettext_lazy("tax_code", "Reverse charge")),
|
||||
)
|
||||
),
|
||||
(
|
||||
_("Tax free"),
|
||||
(
|
||||
# DSFinV-K mapping: 5
|
||||
("O", pgettext_lazy("tax_code", "Services outside of scope of tax")),
|
||||
|
||||
# DSFinV-K mapping: 6
|
||||
("E", pgettext_lazy("tax_code", "Exempt from tax (no reason given)")),
|
||||
|
||||
# DSFinV-K mapping: 6
|
||||
("Z", pgettext_lazy("tax_code", "Zero-rated goods")),
|
||||
|
||||
# DSFinV-K mapping: 5
|
||||
("G", pgettext_lazy("tax_code", "Free export item, VAT not charged")),
|
||||
|
||||
# DSFinV-K mapping: 6?
|
||||
("K", pgettext_lazy("tax_code", "VAT exempt for EEA intra-community supply of goods and services")),
|
||||
)
|
||||
),
|
||||
(
|
||||
_("Special cases"),
|
||||
(
|
||||
("L", pgettext_lazy("tax_code", "Canary Islands general indirect tax")),
|
||||
("M", pgettext_lazy("tax_code", "Tax for production, services and importation in Ceuta and Melilla")),
|
||||
("B", pgettext_lazy("tax_code", "Transferred (VAT), only in Italy")),
|
||||
)
|
||||
),
|
||||
(
|
||||
_("Exempt with specific reason"),
|
||||
(
|
||||
("E/VATEX-EU-79-C",
|
||||
pgettext_lazy("tax_code", "Exempt based on article 79, point c of Council Directive 2006/112/EC")),
|
||||
*[
|
||||
(
|
||||
f"E/VATEX-EU-132-1{letter.upper()}",
|
||||
lazy(
|
||||
lambda let: pgettext(
|
||||
"tax_code",
|
||||
"Exempt based on article {article}, section {section} ({letter}) of Council "
|
||||
"Directive 2006/112/EC"
|
||||
).format(article="132", section="1", letter=let),
|
||||
str
|
||||
)(letter)
|
||||
) for letter in ("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q")
|
||||
],
|
||||
*[
|
||||
(
|
||||
f"E/VATEX-EU-143-1{letter.upper()}",
|
||||
lazy(
|
||||
lambda let: pgettext(
|
||||
"tax_code",
|
||||
"Exempt based on article {article}, section {section} ({letter}) of Council "
|
||||
"Directive 2006/112/EC"
|
||||
).format(article="143", section="1", letter=let),
|
||||
str
|
||||
)(letter)
|
||||
) for letter in ("a", "b", "c", "d", "e", "f", "fa", "g", "h", "i", "j", "k", "l")
|
||||
],
|
||||
*[
|
||||
(
|
||||
f"E/VATEX-EU-148-{letter.upper()}",
|
||||
lazy(
|
||||
lambda let: pgettext(
|
||||
"tax_code",
|
||||
"Exempt based on article {article}, section ({letter}) of Council "
|
||||
"Directive 2006/112/EC"
|
||||
).format(article="148", letter=let),
|
||||
str
|
||||
)(letter)
|
||||
) for letter in ("a", "b", "c", "d", "e", "f", "g")
|
||||
],
|
||||
*[
|
||||
(
|
||||
f"E/VATEX-EU-151-1{letter.upper()}",
|
||||
lazy(
|
||||
lambda let: pgettext(
|
||||
"tax_code",
|
||||
"Exempt based on article {article}, section {section} ({letter}) of Council "
|
||||
"Directive 2006/112/EC"
|
||||
).format(article="151", section="1", letter=let),
|
||||
str
|
||||
)(letter)
|
||||
) for letter in ("a", "aa", "b", "c", "d", "e")
|
||||
],
|
||||
("E/VATEX-EU-309",
|
||||
pgettext_lazy("tax_code", "Exempt based on article 309 of Council Directive 2006/112/EC")),
|
||||
("E/VATEX-EU-D",
|
||||
pgettext_lazy("tax_code", "Intra-Community acquisition from second hand means of transport")),
|
||||
("E/VATEX-EU-F",
|
||||
pgettext_lazy("tax_code", "Intra-Community acquisition of second hand goods")),
|
||||
("E/VATEX-EU-I",
|
||||
pgettext_lazy("tax_code", "Intra-Community acquisition of works of art")),
|
||||
("E/VATEX-EU-J",
|
||||
pgettext_lazy("tax_code", "Intra-Community acquisition of collectors items and antiques")),
|
||||
("E/VATEX-FR-FRANCHISE",
|
||||
pgettext_lazy("tax_code", "France domestic VAT franchise in base")),
|
||||
("E/VATEX-FR-CNWVAT",
|
||||
pgettext_lazy("tax_code", "France domestic Credit Notes without VAT, due to supplier forfeit of VAT for discount")),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def get_tax_code_labels():
|
||||
flat = []
|
||||
for choice, value in TAX_CODE_LISTS:
|
||||
if isinstance(value, (list, tuple)):
|
||||
flat.extend(value)
|
||||
else:
|
||||
flat.append((choice, value))
|
||||
|
||||
return dict(make_hashable(flat))
|
||||
|
||||
|
||||
def is_eu_country(cc):
|
||||
cc = str(cc)
|
||||
return cc in EU_COUNTRIES
|
||||
@@ -173,6 +326,14 @@ class TaxRule(LoggedModel):
|
||||
help_text=_('Should be short, e.g. "VAT"'),
|
||||
max_length=190,
|
||||
)
|
||||
code = models.CharField(
|
||||
verbose_name=_('Tax code'),
|
||||
help_text=_('If you help us understand what this tax rules legally is, we can use this information for '
|
||||
'eInvoices, exporting to accounting system, etc.'),
|
||||
null=True, blank=True,
|
||||
max_length=190,
|
||||
choices=TAX_CODE_LISTS,
|
||||
)
|
||||
rate = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
@@ -250,6 +411,16 @@ class TaxRule(LoggedModel):
|
||||
if self.eu_reverse_charge and not self.home_country:
|
||||
raise ValidationError(_('You need to set your home country to use the reverse charge feature.'))
|
||||
|
||||
if self.rate != Decimal("0.00") and self.code and (self.code.split("/")[0] in ("O", "E", "Z", "G", "K", "AE")):
|
||||
raise ValidationError({
|
||||
"code": _("A combination of this tax code with a non-zero tax rate does not make sense.")
|
||||
})
|
||||
|
||||
if self.rate == Decimal("0.00") and self.code and (self.code.split("/")[0] in ("S", "L", "M", "B")):
|
||||
raise ValidationError({
|
||||
"code": _("A combination of this tax code with a zero tax rate does not make sense.")
|
||||
})
|
||||
|
||||
def __str__(self):
|
||||
if self.price_includes_tax:
|
||||
s = _('incl. {rate}% {name}').format(rate=self.rate, name=self.name)
|
||||
@@ -276,8 +447,9 @@ class TaxRule(LoggedModel):
|
||||
return Decimal(rule.get('rate'))
|
||||
return Decimal(self.rate)
|
||||
|
||||
def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, invoice_address=None,
|
||||
subtract_from_gross=Decimal('0.00'), gross_price_is_tax_rate: Decimal = None, force_fixed_gross_price=False):
|
||||
def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, override_tax_code=None,
|
||||
invoice_address=None, subtract_from_gross=Decimal('0.00'), gross_price_is_tax_rate: Decimal = None,
|
||||
force_fixed_gross_price=False):
|
||||
from .event import Event
|
||||
try:
|
||||
currency = currency or self.event.currency
|
||||
@@ -285,6 +457,13 @@ class TaxRule(LoggedModel):
|
||||
pass
|
||||
|
||||
rate = Decimal(self.rate)
|
||||
code = self.code
|
||||
|
||||
if override_tax_code is not None:
|
||||
code = override_tax_code
|
||||
elif invoice_address:
|
||||
code = self.tax_code_for(invoice_address)
|
||||
|
||||
if override_tax_rate is not None:
|
||||
rate = override_tax_rate
|
||||
elif invoice_address:
|
||||
@@ -317,11 +496,8 @@ class TaxRule(LoggedModel):
|
||||
if rate == Decimal('0.00'):
|
||||
gross = _limit_subtract(base_price, subtract_from_gross)
|
||||
return TaxedPrice(
|
||||
net=gross,
|
||||
gross=gross,
|
||||
tax=Decimal('0.00'),
|
||||
rate=rate,
|
||||
name=self.name,
|
||||
net=gross, gross=gross, tax=Decimal('0.00'),
|
||||
rate=rate, name=self.name, code=code,
|
||||
)
|
||||
|
||||
if base_price_is == 'auto':
|
||||
@@ -346,7 +522,7 @@ class TaxRule(LoggedModel):
|
||||
|
||||
return TaxedPrice(
|
||||
net=net, gross=gross, tax=gross - net,
|
||||
rate=rate, name=self.name
|
||||
rate=rate, name=self.name, code=code,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -427,6 +603,38 @@ class TaxRule(LoggedModel):
|
||||
return True
|
||||
return False
|
||||
|
||||
def tax_code_for(self, invoice_address):
|
||||
if self._custom_rules:
|
||||
rule = self.get_matching_rule(invoice_address)
|
||||
if rule.get("code"):
|
||||
return rule["code"]
|
||||
if rule.get("action", "vat") == "reverse":
|
||||
return "AE"
|
||||
return self.code
|
||||
|
||||
if not self.eu_reverse_charge:
|
||||
# No reverse charge rules? Always apply VAT!
|
||||
return self.code
|
||||
|
||||
if not invoice_address or not invoice_address.country:
|
||||
# No country specified? Always apply VAT!
|
||||
return self.code
|
||||
|
||||
if not is_eu_country(invoice_address.country):
|
||||
# Non-EU country? "Non-taxable" since not in scope
|
||||
return "O"
|
||||
|
||||
if invoice_address.country == self.home_country:
|
||||
# Within same EU country? Always apply VAT!
|
||||
return self.code
|
||||
|
||||
if invoice_address.is_business and invoice_address.vat_id and invoice_address.vat_id_validated:
|
||||
# Reverse charge case
|
||||
return "AE"
|
||||
|
||||
# Consumer in different EU country / invalid VAT
|
||||
return self.code
|
||||
|
||||
def _tax_applicable(self, invoice_address):
|
||||
if self._custom_rules:
|
||||
rule = self.get_matching_rule(invoice_address)
|
||||
|
||||
+60
-33
@@ -722,6 +722,10 @@ class BasePaymentProvider:
|
||||
"""
|
||||
return ""
|
||||
|
||||
def storefrontapi_prepare(self, session_data, total, info):
|
||||
# TODO: docstring
|
||||
return True
|
||||
|
||||
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str]:
|
||||
"""
|
||||
Will be called after the user selects this provider as their payment method.
|
||||
@@ -1419,50 +1423,73 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
||||
return True
|
||||
|
||||
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
|
||||
from pretix.base.services.cart import add_payment_to_cart
|
||||
def _add_giftcard_to_cart(self, cs, gc):
|
||||
from pretix.base.services.cart import add_payment_to_cart_session
|
||||
|
||||
if gc.currency != self.event.currency:
|
||||
raise ValidationError(_("This gift card does not support this currency."))
|
||||
if gc.testmode and not self.event.testmode:
|
||||
raise ValidationError(_("This gift card can only be used in test mode."))
|
||||
if not gc.testmode and self.event.testmode:
|
||||
raise ValidationError(_("Only test gift cards can be used in test mode."))
|
||||
if gc.expires and gc.expires < time_machine_now():
|
||||
raise ValidationError(_("This gift card is no longer valid."))
|
||||
if gc.value <= Decimal("0.00"):
|
||||
raise ValidationError(_("All credit on this gift card has been used."))
|
||||
|
||||
for p in cs.get('payments', []):
|
||||
if p['provider'] == self.identifier and p['info_data']['gift_card'] == gc.pk:
|
||||
raise ValidationError(_("This gift card is already used for your payment."))
|
||||
|
||||
add_payment_to_cart_session(
|
||||
cs,
|
||||
self,
|
||||
max_value=gc.value,
|
||||
info_data={
|
||||
'gift_card': gc.pk,
|
||||
'gift_card_secret': gc.secret,
|
||||
}
|
||||
)
|
||||
|
||||
def storefrontapi_prepare(self, session_data, total, info):
|
||||
# todo: validate gift card not paid with gift card
|
||||
try:
|
||||
gc = self.event.organizer.accepted_gift_cards.get(
|
||||
secret=info.get("giftcard").strip()
|
||||
)
|
||||
try:
|
||||
self._add_giftcard_to_cart(session_data, gc)
|
||||
return True
|
||||
except ValidationError as e:
|
||||
raise PaymentException(str(e.message))
|
||||
except GiftCard.DoesNotExist:
|
||||
if self.event.vouchers.filter(code__iexact=info.get("giftcard")).exists():
|
||||
raise PaymentException(
|
||||
_("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
|
||||
"the product selection.")
|
||||
)
|
||||
else:
|
||||
raise PaymentException(_("This gift card is not known."))
|
||||
except GiftCard.MultipleObjectsReturned:
|
||||
raise PaymentException(_("This gift card can not be redeemed since its code is not unique. Please contact the organizer of this event."))
|
||||
|
||||
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
|
||||
for p in get_cart(request):
|
||||
if p.item.issue_giftcard:
|
||||
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
|
||||
return
|
||||
|
||||
cs = cart_session(request)
|
||||
try:
|
||||
gc = self.event.organizer.accepted_gift_cards.get(
|
||||
secret=request.POST.get("giftcard").strip()
|
||||
)
|
||||
if gc.currency != self.event.currency:
|
||||
messages.error(request, _("This gift card does not support this currency."))
|
||||
cs = cart_session(request)
|
||||
try:
|
||||
self._add_giftcard_to_cart(cs, gc)
|
||||
return True
|
||||
except ValidationError as e:
|
||||
messages.error(request, str(e.message))
|
||||
return
|
||||
if gc.testmode and not self.event.testmode:
|
||||
messages.error(request, _("This gift card can only be used in test mode."))
|
||||
return
|
||||
if not gc.testmode and self.event.testmode:
|
||||
messages.error(request, _("Only test gift cards can be used in test mode."))
|
||||
return
|
||||
if gc.expires and gc.expires < time_machine_now():
|
||||
messages.error(request, _("This gift card is no longer valid."))
|
||||
return
|
||||
if gc.value <= Decimal("0.00"):
|
||||
messages.error(request, _("All credit on this gift card has been used."))
|
||||
return
|
||||
|
||||
for p in cs.get('payments', []):
|
||||
if p['provider'] == self.identifier and p['info_data']['gift_card'] == gc.pk:
|
||||
messages.error(request, _("This gift card is already used for your payment."))
|
||||
return
|
||||
|
||||
add_payment_to_cart(
|
||||
request,
|
||||
self,
|
||||
max_value=gc.value,
|
||||
info_data={
|
||||
'gift_card': gc.pk,
|
||||
'gift_card_secret': gc.secret,
|
||||
}
|
||||
)
|
||||
return True
|
||||
except GiftCard.DoesNotExist:
|
||||
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
|
||||
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
|
||||
|
||||
@@ -1426,6 +1426,28 @@ class CartManager:
|
||||
raise CartError(err)
|
||||
|
||||
|
||||
def add_payment_to_cart_session(cart_session, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
||||
"""
|
||||
:param cart_session: The current cart session.
|
||||
:param provider: The instance of your payment provider.
|
||||
:param min_value: The minimum value this payment instrument supports, or ``None`` for unlimited.
|
||||
:param max_value: The maximum value this payment instrument supports, or ``None`` for unlimited. Highly discouraged
|
||||
to use for payment providers which charge a payment fee, as this can be very user-unfriendly if
|
||||
users need a second payment method just for the payment fee of the first method.
|
||||
:param info_data: A dictionary of information that will be passed through to the ``OrderPayment.info_data`` attribute.
|
||||
:return:
|
||||
"""
|
||||
cart_session.setdefault('payments', [])
|
||||
cart_session['payments'].append({
|
||||
'id': str(uuid.uuid4()),
|
||||
'provider': provider.identifier,
|
||||
'multi_use_supported': provider.multi_use_supported,
|
||||
'min_value': str(min_value) if min_value is not None else None,
|
||||
'max_value': str(max_value) if max_value is not None else None,
|
||||
'info_data': info_data or {},
|
||||
})
|
||||
|
||||
|
||||
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
||||
"""
|
||||
:param request: The current HTTP request context.
|
||||
@@ -1440,16 +1462,7 @@ def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: D
|
||||
from pretix.presale.views.cart import cart_session
|
||||
|
||||
cs = cart_session(request)
|
||||
cs.setdefault('payments', [])
|
||||
|
||||
cs['payments'].append({
|
||||
'id': str(uuid.uuid4()),
|
||||
'provider': provider.identifier,
|
||||
'multi_use_supported': provider.multi_use_supported,
|
||||
'min_value': str(min_value) if min_value is not None else None,
|
||||
'max_value': str(max_value) if max_value is not None else None,
|
||||
'info_data': info_data or {},
|
||||
})
|
||||
add_payment_to_cart_session(cs, provider, min_value, max_value, info_data)
|
||||
|
||||
|
||||
def get_fees(event, request, total, invoice_address, payments, positions):
|
||||
@@ -1500,6 +1513,7 @@ def get_fees(event, request, total, invoice_address, payments, positions):
|
||||
value=payment_fee,
|
||||
tax_rate=payment_fee_tax.rate,
|
||||
tax_value=payment_fee_tax.tax,
|
||||
tax_code=payment_fee_tax.code,
|
||||
tax_rule=payment_fee_tax_rule
|
||||
))
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.db.models import Exists, OuterRef
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
@@ -32,6 +33,7 @@ from pretix.base.models.customers import CustomerSSOGrant
|
||||
|
||||
from ..models import CachedFile, CartPosition, InvoiceAddress
|
||||
from ..models.auth import UserKnownLoginSource
|
||||
from ..models.orders import CheckoutSession
|
||||
from ..signals import periodic_task
|
||||
|
||||
|
||||
@@ -42,6 +44,10 @@ def clean_cart_positions(sender, **kwargs):
|
||||
cp.delete()
|
||||
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=True):
|
||||
cp.delete()
|
||||
for cs in CheckoutSession.objects.filter(created__lt=now() - timedelta(days=14)).exclude(
|
||||
Exists(CartPosition.objects.filter(cart_id=OuterRef("cart_id")))
|
||||
):
|
||||
cs.delete()
|
||||
for ia in InvoiceAddress.objects.filter(order__isnull=True, customer__isnull=True, last_modified__lt=now() - timedelta(days=14)):
|
||||
ia.delete()
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ from typing import List
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from pretix.base.models import CartPosition, ItemCategory, SalesChannel
|
||||
from pretix.presale.views.event import get_grouped_items
|
||||
from pretix.base.storelogic.products import get_items_for_product_list
|
||||
|
||||
|
||||
class DummyCategory:
|
||||
@@ -161,7 +161,7 @@ class CrossSellingService:
|
||||
]
|
||||
|
||||
def _prepare_items(self, subevent, items_qs, discount_info):
|
||||
items, _btn = get_grouped_items(
|
||||
items, _btn = get_items_for_product_list(
|
||||
self.event,
|
||||
subevent=subevent,
|
||||
voucher=None,
|
||||
|
||||
@@ -271,7 +271,9 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
event_date_from=p.subevent.date_from if invoice.event.has_subevents else invoice.event.date_from,
|
||||
event_date_to=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to,
|
||||
event_location=location if invoice.event.settings.invoice_event_location else None,
|
||||
tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
|
||||
tax_rate=p.tax_rate,
|
||||
tax_code=p.tax_code,
|
||||
tax_name=p.tax_rule.name if p.tax_rule else ''
|
||||
)
|
||||
|
||||
if p.tax_rule and p.tax_rule.is_reverse_charge(ia) and p.price and not p.tax_value:
|
||||
@@ -305,6 +307,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
),
|
||||
tax_value=fee.tax_value,
|
||||
tax_rate=fee.tax_rate,
|
||||
tax_code=fee.tax_code,
|
||||
tax_name=fee.tax_rule.name if fee.tax_rule else '',
|
||||
fee_type=fee.fee_type,
|
||||
fee_internal_type=fee.internal_type or None,
|
||||
@@ -491,13 +494,13 @@ def build_preview_invoice_pdf(event):
|
||||
InvoiceLine.objects.create(
|
||||
invoice=invoice, description=_("Sample product {}").format(i + 1),
|
||||
gross_value=tax.gross, tax_value=tax.tax,
|
||||
tax_rate=tax.rate, tax_name=tax.name
|
||||
tax_rate=tax.rate, tax_name=tax.name, tax_code=tax.code,
|
||||
)
|
||||
else:
|
||||
for i in range(5):
|
||||
InvoiceLine.objects.create(
|
||||
invoice=invoice, description=_("Sample product A"),
|
||||
gross_value=100, tax_value=0, tax_rate=0
|
||||
gross_value=100, tax_value=0, tax_rate=0, tax_code=None,
|
||||
)
|
||||
|
||||
return event.invoice_renderer.generate(invoice)
|
||||
|
||||
@@ -76,7 +76,7 @@ from pretix.base.services.tasks import TransactionAwareTask
|
||||
from pretix.base.services.tickets import get_tickets_for_order
|
||||
from pretix.base.signals import email_filter, global_email_filter
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
from pretix.helpers.hierarkey import clean_filename
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.presale.ical import get_private_icals
|
||||
@@ -311,7 +311,13 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
try:
|
||||
if plain_text_only:
|
||||
body_html = None
|
||||
elif 'context' in inspect.signature(renderer.render).parameters:
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
|
||||
elif 'position' in inspect.signature(renderer.render).parameters:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without context argument because context argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
||||
else:
|
||||
# Backwards compatibility
|
||||
@@ -323,6 +329,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
logger.exception('Could not render HTML body')
|
||||
body_html = None
|
||||
|
||||
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
|
||||
send_task = mail_send_task.si(
|
||||
to=[email] if isinstance(email, str) else list(email),
|
||||
cc=cc,
|
||||
@@ -655,7 +663,7 @@ def render_mail(template, context):
|
||||
if isinstance(template, LazyI18nString):
|
||||
body = str(template)
|
||||
if context:
|
||||
body = format_map(body, context)
|
||||
body = format_map(body, context, mode=SafeFormatter.MODE_IGNORE_RICH)
|
||||
else:
|
||||
tpl = get_template(template)
|
||||
body = tpl.render(context)
|
||||
|
||||
@@ -118,7 +118,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
|
||||
c.assign(record.get(c.identifier), order, position, order._address)
|
||||
|
||||
if position.seat is not None:
|
||||
lock_seats.append(position.seat)
|
||||
lock_seats.append((order.sales_channel, position.seat))
|
||||
except (ValidationError, ImportError) as e:
|
||||
raise DataImportError(
|
||||
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
|
||||
@@ -128,9 +128,9 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
|
||||
with transaction.atomic():
|
||||
# We don't support vouchers, quotas, or memberships here, so we only need to lock if seats are in use
|
||||
if lock_seats:
|
||||
lock_objects(lock_seats, shared_lock_objects=[event])
|
||||
for s in lock_seats:
|
||||
if not s.is_available():
|
||||
lock_objects([s for c, s in lock_seats], shared_lock_objects=[event])
|
||||
for c, s in lock_seats:
|
||||
if not s.is_available(sales_channel=c):
|
||||
raise DataImportError(_('The seat you selected has already been taken. Please select a different seat.'))
|
||||
|
||||
save_transactions = []
|
||||
|
||||
@@ -1721,16 +1721,17 @@ class OrderChangeManager:
|
||||
|
||||
try:
|
||||
new_rate = tax_rule.tax_rate_for(ia)
|
||||
new_code = tax_rule.tax_code_for(ia)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
raise OrderError(error_messages['tax_rule_country_blocked'])
|
||||
# We use override_tax_rate to make sure .tax() doesn't get clever and re-adjusts the pricing itself
|
||||
if new_rate != pos.tax_rate:
|
||||
if new_rate != pos.tax_rate or new_code != pos.tax_code:
|
||||
if keep == 'net':
|
||||
new_tax = tax_rule.tax(pos.price - pos.tax_value, base_price_is='net', currency=self.event.currency,
|
||||
override_tax_rate=new_rate)
|
||||
override_tax_rate=new_rate, override_tax_code=new_code)
|
||||
else:
|
||||
new_tax = tax_rule.tax(pos.price, base_price_is='gross', currency=self.event.currency,
|
||||
override_tax_rate=new_rate)
|
||||
override_tax_rate=new_rate, override_tax_code=new_code)
|
||||
self._totaldiff += new_tax.gross - pos.price
|
||||
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
|
||||
self._invoice_dirty = True
|
||||
@@ -2304,6 +2305,7 @@ class OrderChangeManager:
|
||||
op.position.price = op.price.gross
|
||||
op.position.tax_rate = op.price.rate
|
||||
op.position.tax_value = op.price.tax
|
||||
op.position.tax_code = op.price.code
|
||||
op.position.save()
|
||||
elif isinstance(op, self.TaxRuleOperation):
|
||||
if isinstance(op.position, OrderPosition):
|
||||
@@ -2400,7 +2402,7 @@ class OrderChangeManager:
|
||||
elif isinstance(op, self.AddOperation):
|
||||
pos = OrderPosition.objects.create(
|
||||
item=op.item, variation=op.variation, addon_to=op.addon_to,
|
||||
price=op.price.gross, order=self.order, tax_rate=op.price.rate,
|
||||
price=op.price.gross, order=self.order, tax_rate=op.price.rate, tax_code=op.price.code,
|
||||
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
|
||||
positionid=nextposid, subevent=op.subevent, seat=op.seat,
|
||||
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
|
||||
@@ -2423,6 +2425,8 @@ class OrderChangeManager:
|
||||
elif isinstance(op, self.SplitOperation):
|
||||
split_positions.append(op.position)
|
||||
elif isinstance(op, self.RegenerateSecretOperation):
|
||||
op.position.web_secret = generate_secret()
|
||||
op.position.save(update_fields=["web_secret"])
|
||||
assign_ticket_secret(
|
||||
event=self.event, position=op.position, force_invalidate=True, save=True
|
||||
)
|
||||
@@ -2529,6 +2533,7 @@ class OrderChangeManager:
|
||||
'new_order': split_order.code,
|
||||
})
|
||||
op.order = split_order
|
||||
op.web_secret = generate_secret()
|
||||
assign_ticket_secret(
|
||||
self.event, position=op, force_invalidate=True,
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ from decimal import Decimal
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -39,7 +40,8 @@ from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
|
||||
from pretix.base.signals import (
|
||||
register_mail_placeholders, register_text_placeholders,
|
||||
)
|
||||
from pretix.helpers.format import SafeFormatter
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.helpers.format import PlainHtmlAlternativeString, SafeFormatter
|
||||
|
||||
logger = logging.getLogger('pretix.base.services.placeholders')
|
||||
|
||||
@@ -107,6 +109,91 @@ class SimpleFunctionalTextPlaceholder(BaseTextPlaceholder):
|
||||
return self._sample
|
||||
|
||||
|
||||
class BaseRichTextPlaceholder(BaseTextPlaceholder):
|
||||
"""
|
||||
This is the base class for all placeholders which can render either to plain text
|
||||
or to a rich HTML element.
|
||||
"""
|
||||
|
||||
def __init__(self, identifier, args):
|
||||
self._identifier = identifier
|
||||
self._args = args
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return self._identifier
|
||||
|
||||
@property
|
||||
def required_context(self):
|
||||
return self._args
|
||||
|
||||
@property
|
||||
def is_block(self):
|
||||
return False
|
||||
|
||||
def render(self, context):
|
||||
return PlainHtmlAlternativeString(
|
||||
self.render_plain(**{k: context[k] for k in self._args}),
|
||||
self.render_html(**{k: context[k] for k in self._args}),
|
||||
self.is_block,
|
||||
)
|
||||
|
||||
def render_html(self, **kwargs):
|
||||
"""
|
||||
HTML rendering of the placeholder. Should return "safe" HTML, i.e. everything needs to be
|
||||
escaped.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def render_plain(self, **kwargs):
|
||||
"""
|
||||
Plain text rendering of the placeholder.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def render_sample(self, event):
|
||||
return PlainHtmlAlternativeString(
|
||||
self.render_sample_plain(event=event),
|
||||
self.render_sample_html(event=event),
|
||||
self.is_block,
|
||||
)
|
||||
|
||||
def render_sample_html(self, event):
|
||||
raise NotImplementedError
|
||||
|
||||
def render_sample_plain(self, event):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SimpleButtonPlaceholder(BaseRichTextPlaceholder):
|
||||
def __init__(self, identifier, args, url_func, text_func, sample_url_func, sample_text_func):
|
||||
super().__init__(identifier, args)
|
||||
self._url_func = url_func
|
||||
self._text_func = text_func
|
||||
self._sample_url_func = sample_url_func
|
||||
self._sample_text_func = sample_text_func
|
||||
|
||||
def render_html(self, **context):
|
||||
text = self._text_func(**{k: context[k] for k in self._args})
|
||||
url = self._url_func(**{k: context[k] for k in self._args})
|
||||
return f'<a href="{url}" class="button">{escape(text)}</a>'
|
||||
|
||||
def render_plain(self, **context):
|
||||
text = self._text_func(**{k: context[k] for k in self._args})
|
||||
url = self._url_func(**{k: context[k] for k in self._args})
|
||||
return f'{text}: {url}'
|
||||
|
||||
def render_sample_html(self, event):
|
||||
text = self._sample_text_func(event)
|
||||
url = self._sample_url_func(event)
|
||||
return f'<a href="{url}" class="button">{escape(text)}</a>'
|
||||
|
||||
def render_sample_plain(self, event):
|
||||
text = self._sample_text_func(event)
|
||||
url = self._sample_url_func(event)
|
||||
return f'{text}: {url}'
|
||||
|
||||
|
||||
class PlaceholderContext(SafeFormatter):
|
||||
"""
|
||||
Holds the contextual arguments and corresponding list of available placeholders for formatting
|
||||
@@ -284,6 +371,27 @@ def base_placeholders(sender, **kwargs):
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleButtonPlaceholder(
|
||||
'url_button', ['order', 'event'],
|
||||
url_func=lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.open', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_secret()
|
||||
}
|
||||
),
|
||||
text_func=lambda order, event: _("View order details"),
|
||||
sample_url_func=lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.open', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'hash': '98kusd8ofsj8dnkd'
|
||||
}
|
||||
),
|
||||
sample_text_func=lambda event: _("View order details"),
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
@@ -348,6 +456,27 @@ def base_placeholders(sender, **kwargs):
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleButtonPlaceholder(
|
||||
'url_button', ['event', 'position'],
|
||||
url_func=lambda event, position: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position', kwargs={
|
||||
'order': position.order.code,
|
||||
'secret': position.web_secret,
|
||||
'position': position.positionid
|
||||
}
|
||||
),
|
||||
text_func=lambda event, position: _("View registration details"),
|
||||
sample_url_func=lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'position': '123'
|
||||
}
|
||||
),
|
||||
sample_text_func=lambda event: _("View registration details"),
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||
event,
|
||||
@@ -603,8 +732,8 @@ def base_placeholders(sender, **kwargs):
|
||||
|
||||
|
||||
class FormPlaceholderMixin:
|
||||
def _set_field_placeholders(self, fn, base_parameters):
|
||||
placeholders = get_available_placeholders(self.event, base_parameters)
|
||||
def _set_field_placeholders(self, fn, base_parameters, rich=False):
|
||||
placeholders = get_available_placeholders(self.event, base_parameters, rich=rich)
|
||||
ht = format_placeholders_help_text(placeholders, self.event)
|
||||
if self.fields[fn].help_text:
|
||||
self.fields[fn].help_text += ' ' + str(ht)
|
||||
@@ -615,7 +744,7 @@ class FormPlaceholderMixin:
|
||||
)
|
||||
|
||||
|
||||
def get_available_placeholders(event, base_parameters):
|
||||
def get_available_placeholders(event, base_parameters, rich=False):
|
||||
if 'order' in base_parameters:
|
||||
base_parameters.append('invoice_address')
|
||||
base_parameters.append('position_or_address')
|
||||
@@ -624,6 +753,35 @@ def get_available_placeholders(event, base_parameters):
|
||||
if not isinstance(val, (list, tuple)):
|
||||
val = [val]
|
||||
for v in val:
|
||||
if isinstance(v, BaseRichTextPlaceholder) and not rich:
|
||||
continue
|
||||
if all(rp in base_parameters for rp in v.required_context):
|
||||
params[v.identifier] = v
|
||||
return params
|
||||
|
||||
|
||||
def get_sample_context(event, context_parameters, rich=True):
|
||||
context_dict = {}
|
||||
lbl = _('This value will be replaced based on dynamic parameters.')
|
||||
for k, v in get_available_placeholders(event, context_parameters, rich=rich).items():
|
||||
sample = v.render_sample(event)
|
||||
if isinstance(sample, PlainHtmlAlternativeString):
|
||||
context_dict[k] = PlainHtmlAlternativeString(
|
||||
sample.plain,
|
||||
'<{el} class="placeholder placeholder-html" title="{title}">{html}</{el}>'.format(
|
||||
el='div' if sample.is_block else 'span',
|
||||
title=lbl,
|
||||
html=sample.html,
|
||||
)
|
||||
)
|
||||
elif str(sample).strip().startswith('* ') or str(sample).startswith(' '):
|
||||
context_dict[k] = '<div class="placeholder" title="{}">{}</div>'.format(
|
||||
lbl,
|
||||
markdown_compile_email(str(sample))
|
||||
)
|
||||
else:
|
||||
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||
lbl,
|
||||
escape(sample)
|
||||
)
|
||||
return context_dict
|
||||
|
||||
@@ -91,9 +91,11 @@ def get_price(item: Item, variation: ItemVariation = None,
|
||||
|
||||
if custom_price_is_net:
|
||||
price = tax_rule.tax(max(custom_price, price.net), base_price_is='net', override_tax_rate=price.rate,
|
||||
override_tax_code=price.code,
|
||||
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,
|
||||
override_tax_code=price.code,
|
||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
else:
|
||||
price = tax_rule.tax(price, invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
@@ -146,10 +148,12 @@ def get_line_price(price_after_voucher: Decimal, custom_price_input: Decimal, cu
|
||||
|
||||
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)
|
||||
override_tax_code=price.code, 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)
|
||||
override_tax_code=price.code, 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,
|
||||
base_price_is='gross' if is_bundled else 'auto')
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#
|
||||
import logging
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.timezone import now
|
||||
@@ -97,9 +98,9 @@ def preview(event: int, provider: str):
|
||||
event = Event.objects.get(id=event)
|
||||
|
||||
with rolledback_transaction(), language(event.settings.locale, event.settings.region):
|
||||
item = event.items.create(name=_("Sample product"), default_price=42.23,
|
||||
item = event.items.create(name=_("Sample product"), default_price=Decimal('42.23'),
|
||||
description=_("Sample product description"))
|
||||
item2 = event.items.create(name=_("Sample workshop"), default_price=23.40)
|
||||
item2 = event.items.create(name=_("Sample workshop"), default_price=Decimal('23.40'))
|
||||
|
||||
from pretix.base.models import Order
|
||||
order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
||||
|
||||
@@ -56,6 +56,7 @@ from django.utils.translation import (
|
||||
from django_countries.fields import Country
|
||||
from hierarkey.models import GlobalSettingsBase, Hierarkey
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
from i18nfield.rest_framework import I18nField
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from phonenumbers import PhoneNumber, parse
|
||||
from rest_framework import serializers
|
||||
@@ -63,7 +64,7 @@ from rest_framework import serializers
|
||||
from pretix.api.serializers.fields import (
|
||||
ListMultipleChoiceField, UploadedFileField,
|
||||
)
|
||||
from pretix.api.serializers.i18n import I18nField, I18nURLField
|
||||
from pretix.api.serializers.i18n import I18nURLField
|
||||
from pretix.base.forms import I18nMarkdownTextarea, I18nURLFormField
|
||||
from pretix.base.models.tax import VAT_ID_COUNTRIES, TaxRule
|
||||
from pretix.base.reldate import (
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
class IncompleteError(Exception):
|
||||
pass
|
||||
@@ -0,0 +1,118 @@
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
from pretix.base.storelogic.products import get_items_for_product_list
|
||||
|
||||
|
||||
def addons_is_completed(cart_positions):
|
||||
for cartpos in cart_positions.filter(addon_to__isnull=True).prefetch_related(
|
||||
'item__addons', 'item__addons__addon_category', 'addons', 'addons__item'
|
||||
):
|
||||
a = cartpos.addons.all()
|
||||
for iao in cartpos.item.addons.all():
|
||||
found = len([1 for p in a if p.item.category_id == iao.addon_category_id and not p.is_bundled])
|
||||
if found < iao.min_count or found > iao.max_count:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def addons_is_applicable(cart_positions):
|
||||
return cart_positions.filter(item__addons__isnull=False).exists()
|
||||
|
||||
|
||||
def get_addon_groups(event, sales_channel, customer, cart_positions):
|
||||
quota_cache = {}
|
||||
item_cache = {}
|
||||
groups = []
|
||||
for cartpos in sorted(cart_positions.filter(addon_to__isnull=True).prefetch_related(
|
||||
'item__addons', 'item__addons__addon_category', 'addons', 'addons__variation',
|
||||
), key=lambda c: c.sort_key):
|
||||
groupentry = {
|
||||
'pos': cartpos,
|
||||
'item': cartpos.item,
|
||||
'variation': cartpos.variation,
|
||||
'categories': []
|
||||
}
|
||||
|
||||
current_addon_products = defaultdict(list)
|
||||
for a in cartpos.addons.all():
|
||||
if not a.is_bundled:
|
||||
current_addon_products[a.item_id, a.variation_id].append(a)
|
||||
|
||||
for iao in cartpos.item.addons.all():
|
||||
ckey = '{}-{}'.format(cartpos.subevent.pk if cartpos.subevent else 0, iao.addon_category.pk)
|
||||
|
||||
if ckey not in item_cache:
|
||||
# Get all items to possibly show
|
||||
items, _btn = get_items_for_product_list(
|
||||
event,
|
||||
subevent=cartpos.subevent,
|
||||
voucher=None,
|
||||
channel=sales_channel,
|
||||
base_qs=iao.addon_category.items,
|
||||
allow_addons=True,
|
||||
quota_cache=quota_cache,
|
||||
memberships=(
|
||||
customer.usable_memberships(
|
||||
for_event=cartpos.subevent or event,
|
||||
testmode=event.testmode
|
||||
)
|
||||
if customer else None
|
||||
),
|
||||
)
|
||||
item_cache[ckey] = items
|
||||
else:
|
||||
# We can use the cache to prevent a database fetch, but we need separate Python objects
|
||||
# or our things below like setting `i.initial` will do the wrong thing.
|
||||
items = [copy.copy(i) for i in item_cache[ckey]]
|
||||
for i in items:
|
||||
i.available_variations = [copy.copy(v) for v in i.available_variations]
|
||||
|
||||
for i in items:
|
||||
i.allow_waitinglist = False
|
||||
|
||||
if i.has_variations:
|
||||
for v in i.available_variations:
|
||||
v.initial = len(current_addon_products[i.pk, v.pk])
|
||||
if v.initial and i.free_price:
|
||||
a = current_addon_products[i.pk, v.pk][0]
|
||||
v.initial_price = TaxedPrice(
|
||||
net=a.price - a.tax_value,
|
||||
gross=a.price,
|
||||
tax=a.tax_value,
|
||||
name=a.item.tax_rule.name if a.item.tax_rule else "",
|
||||
rate=a.tax_rate,
|
||||
code=a.item.tax_rule.code if a.item.tax_rule else None,
|
||||
)
|
||||
else:
|
||||
v.initial_price = v.suggested_price
|
||||
i.expand = any(v.initial for v in i.available_variations)
|
||||
else:
|
||||
i.initial = len(current_addon_products[i.pk, None])
|
||||
if i.initial and i.free_price:
|
||||
a = current_addon_products[i.pk, None][0]
|
||||
i.initial_price = TaxedPrice(
|
||||
net=a.price - a.tax_value,
|
||||
gross=a.price,
|
||||
tax=a.tax_value,
|
||||
name=a.item.tax_rule.name if a.item.tax_rule else "",
|
||||
rate=a.tax_rate,
|
||||
code=a.item.tax_rule.code if a.item.tax_rule else None,
|
||||
)
|
||||
else:
|
||||
i.initial_price = i.suggested_price
|
||||
|
||||
if items:
|
||||
groupentry['categories'].append({
|
||||
'category': iao.addon_category,
|
||||
'price_included': iao.price_included or (cartpos.voucher_id and cartpos.voucher.all_addons_included),
|
||||
'multi_allowed': iao.multi_allowed,
|
||||
'min_count': iao.min_count,
|
||||
'max_count': iao.max_count,
|
||||
'iao': iao,
|
||||
'items': items
|
||||
})
|
||||
if groupentry['categories']:
|
||||
groups.append(groupentry)
|
||||
return groups
|
||||
@@ -0,0 +1,271 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import EmailValidator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.models import CartPosition, Question
|
||||
from pretix.base.services.checkin import _save_answers
|
||||
from pretix.base.storelogic import IncompleteError
|
||||
from pretix.presale.signals import question_form_fields
|
||||
|
||||
|
||||
class Field:
|
||||
@property
|
||||
def identifier(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def help_text(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def required(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def validation_hints(self):
|
||||
raise {}
|
||||
|
||||
def validate_input(self, value):
|
||||
return value
|
||||
|
||||
|
||||
class PositionField(Field):
|
||||
def save_input(self, position, value):
|
||||
raise NotImplementedError()
|
||||
|
||||
def current_value(self, position):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SessionField(Field):
|
||||
def save_input(self, session_data, value):
|
||||
raise NotImplementedError()
|
||||
|
||||
def current_value(self, session_data):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class QuestionField(PositionField):
|
||||
def __init__(self, question: Question):
|
||||
self.question = question
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return self.question.question
|
||||
|
||||
@property
|
||||
def help_text(self):
|
||||
return self.question.help_text
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self.question.type
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return f"question_{self.question.identifier}"
|
||||
|
||||
def validate_input(self, value):
|
||||
return self.question.clean_answer(value)
|
||||
|
||||
def required(self, value):
|
||||
return self.question.required
|
||||
|
||||
def validation_hints(self):
|
||||
d = {
|
||||
"valid_number_min": self.question.valid_number_min,
|
||||
"valid_number_max": self.question.valid_number_max,
|
||||
"valid_date_min": self.question.valid_date_min,
|
||||
"valid_date_max": self.question.valid_date_max,
|
||||
"valid_datetime_min": self.question.valid_datetime_min,
|
||||
"valid_datetime_max": self.question.valid_datetime_max,
|
||||
"valid_string_length_max": self.question.valid_string_length_max,
|
||||
"dependency_on": f"question_{self.question.dependency_question.identifier}" if self.question.dependency_question_id else None,
|
||||
"dependency_values": self.question.dependency_values,
|
||||
}
|
||||
if self.question.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||
d["choices"] = [
|
||||
{
|
||||
"identifier": opt.identifier,
|
||||
"label": str(opt.answer)
|
||||
}
|
||||
for opt in self.question.options.all()
|
||||
]
|
||||
return d
|
||||
|
||||
def save_input(self, position, value):
|
||||
answers = [a for a in position.answerlist if a.question_id == self.question.id]
|
||||
if answers:
|
||||
answers = {self.question: answers[0]}
|
||||
else:
|
||||
answers = {}
|
||||
_save_answers(position, answers, {self.question: value})
|
||||
|
||||
def current_value(self, position):
|
||||
answers = [a for a in position.answerlist if a.question_id == self.question.id]
|
||||
if answers:
|
||||
if self.question.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||
return ",".join([a.idenitifer for a in answers[0].options.all()])
|
||||
else:
|
||||
return answers[0].answer
|
||||
|
||||
|
||||
class SyntheticSessionField(SessionField):
|
||||
def __init__(self, label, help_text, type, identifier, required, save_func, get_func, validate_func):
|
||||
self._label = label
|
||||
self._help_text = help_text
|
||||
self._type = type
|
||||
self._identifier = identifier
|
||||
self._required = required
|
||||
self._save_func = save_func
|
||||
self._get_func = get_func
|
||||
self._validate_func = validate_func
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return self._label
|
||||
|
||||
@property
|
||||
def help_text(self):
|
||||
return self._help_text
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def required(self):
|
||||
return self._required
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return self._identifier
|
||||
|
||||
def validation_hints(self):
|
||||
return {}
|
||||
|
||||
def save_input(self, session_data, value):
|
||||
self._save_func(session_data, value)
|
||||
|
||||
def current_value(self, session_data):
|
||||
return self._get_func(session_data)
|
||||
|
||||
def validate_input(self, value):
|
||||
return self._validate_func(value)
|
||||
|
||||
|
||||
def get_checkout_fields(event):
|
||||
fields = []
|
||||
# TODO: support contact_form_fields
|
||||
# TODO: support contact_form_fields_override
|
||||
|
||||
# email
|
||||
fields.append(SyntheticSessionField(
|
||||
label=_("Email"),
|
||||
help_text=None,
|
||||
type=Question.TYPE_STRING, # TODO: Add a type?
|
||||
identifier="email",
|
||||
required=True,
|
||||
get_func=lambda session_data: session_data.get("email"),
|
||||
save_func=lambda session_data, value: session_data.update({"email": value}),
|
||||
validate_func=lambda value: EmailValidator()(value) or value,
|
||||
))
|
||||
|
||||
# TODO: phone
|
||||
# TODO: invoice address
|
||||
return fields
|
||||
|
||||
|
||||
def get_position_fields(event, pos: CartPosition):
|
||||
# TODO: support override sets
|
||||
fields = []
|
||||
|
||||
for q in pos.item.questions_to_ask:
|
||||
fields.append(QuestionField(q))
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def ensure_fields_are_completed(event, positions, cart_session, invoice_address, all_optional, cart_is_free):
|
||||
try:
|
||||
emailval = EmailValidator()
|
||||
if not cart_session.get('email') and not all_optional:
|
||||
raise IncompleteError(_('Please enter a valid email address.'))
|
||||
if cart_session.get('email'):
|
||||
emailval(cart_session.get('email'))
|
||||
except ValidationError:
|
||||
raise IncompleteError(_('Please enter a valid email address.'))
|
||||
|
||||
address_asked = (
|
||||
event.settings.invoice_address_asked and (not event.settings.invoice_address_not_asked_free or not cart_is_free)
|
||||
)
|
||||
|
||||
if not all_optional:
|
||||
if address_asked:
|
||||
if event.settings.invoice_address_required and (not invoice_address or not invoice_address.street):
|
||||
raise IncompleteError(_('Please enter your invoicing address.'))
|
||||
|
||||
if event.settings.invoice_name_required and (not invoice_address or not invoice_address.name):
|
||||
raise IncompleteError(_('Please enter your name.'))
|
||||
|
||||
for cp in positions:
|
||||
answ = {
|
||||
aw.question_id: aw for aw in cp.answerlist
|
||||
}
|
||||
question_cache = {
|
||||
q.pk: q for q in cp.item.questions_to_ask
|
||||
}
|
||||
|
||||
def question_is_visible(parentid, qvals):
|
||||
if parentid not in question_cache:
|
||||
return False
|
||||
parentq = question_cache[parentid]
|
||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id,
|
||||
parentq.dependency_values):
|
||||
return False
|
||||
if parentid not in answ:
|
||||
return False
|
||||
return (
|
||||
('True' in qvals and answ[parentid].answer == 'True')
|
||||
or ('False' in qvals and answ[parentid].answer == 'False')
|
||||
or (any(qval in [o.identifier for o in answ[parentid].options.all()] for qval in qvals))
|
||||
)
|
||||
|
||||
def question_is_required(q):
|
||||
return (
|
||||
q.required and
|
||||
(not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_values))
|
||||
)
|
||||
|
||||
if not all_optional:
|
||||
for q in cp.item.questions_to_ask:
|
||||
if question_is_required(q) and q.id not in answ:
|
||||
raise IncompleteError(_('Please fill in answers to all required questions.'))
|
||||
if cp.item.ask_attendee_data and event.settings.get('attendee_names_required', as_type=bool) \
|
||||
and not cp.attendee_name_parts:
|
||||
raise IncompleteError(_('Please fill in answers to all required questions.'))
|
||||
if cp.item.ask_attendee_data and event.settings.get('attendee_emails_required', as_type=bool) \
|
||||
and cp.attendee_email is None:
|
||||
raise IncompleteError(_('Please fill in answers to all required questions.'))
|
||||
if cp.item.ask_attendee_data and event.settings.get('attendee_company_required', as_type=bool) \
|
||||
and cp.company is None:
|
||||
raise IncompleteError(_('Please fill in answers to all required questions.'))
|
||||
if cp.item.ask_attendee_data and event.settings.get('attendee_addresses_required', as_type=bool) \
|
||||
and (cp.street is None and cp.city is None and cp.country is None):
|
||||
raise IncompleteError(_('Please fill in answers to all required questions.'))
|
||||
|
||||
responses = question_form_fields.send(sender=event, position=cp)
|
||||
form_data = cp.meta_info_data.get('question_form_data', {})
|
||||
for r, response in sorted(responses, key=lambda r: str(r[0])):
|
||||
for key, value in response.items():
|
||||
if value.required and not form_data.get(key):
|
||||
raise IncompleteError(_('Please fill in answers to all required questions.'))
|
||||
@@ -0,0 +1,132 @@
|
||||
import copy
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from pretix.base.storelogic import IncompleteError
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
|
||||
def payment_is_applicable(event, total, cart_positions, invoice_address, cart_session, request):
|
||||
for cartpos in cart_positions:
|
||||
if cartpos.requires_approval(invoice_address=invoice_address):
|
||||
if 'payments' in cart_session:
|
||||
del cart_session['payments']
|
||||
return False
|
||||
|
||||
used_providers = {p['provider'] for p in cart_session.get('payments', [])}
|
||||
for provider in event.get_payment_providers().values():
|
||||
if provider.is_implicit(request) if callable(provider.is_implicit) else provider.is_implicit:
|
||||
# TODO: do we need a different is_allowed for storefrontapi?
|
||||
if provider.is_allowed(request, total=total):
|
||||
cart_session['payments'] = [
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'provider': provider.identifier,
|
||||
'multi_use_supported': False,
|
||||
'min_value': None,
|
||||
'max_value': None,
|
||||
'info_data': {},
|
||||
}
|
||||
]
|
||||
return False
|
||||
elif provider.identifier in used_providers:
|
||||
# is_allowed might have changed, e.g. after add-on selection
|
||||
cart_session['payments'] = [p for p in cart_session['payments'] if
|
||||
p['provider'] != provider.identifier]
|
||||
return True
|
||||
|
||||
|
||||
def current_selected_payments(event, total, cart_session, total_includes_payment_fees=False, fail=False):
|
||||
def _remove_payment(payment_id):
|
||||
cart_session['payments'] = [p for p in cart_session['payments'] if p.get('id') != payment_id]
|
||||
|
||||
raw_payments = copy.deepcopy(cart_session.get('payments', []))
|
||||
payments = []
|
||||
total_remaining = total
|
||||
for p in raw_payments:
|
||||
# This algorithm of treating min/max values and fees needs to stay in sync between the following
|
||||
# places in the code base:
|
||||
# - pretix.base.services.cart.get_fees
|
||||
# - pretix.base.services.orders._get_fees
|
||||
# - pretix.presale.storelogic.payment.current_selected_payments
|
||||
if p.get('min_value') and total_remaining < Decimal(p['min_value']):
|
||||
_remove_payment(p['id'])
|
||||
if fail:
|
||||
raise IncompleteError(
|
||||
_('Your selected payment method can only be used for a payment of at least {amount}.').format(
|
||||
amount=money_filter(Decimal(p['min_value']), event.currency)
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
to_pay = total_remaining
|
||||
if p.get('max_value') and to_pay > Decimal(p['max_value']):
|
||||
to_pay = min(to_pay, Decimal(p['max_value']))
|
||||
|
||||
pprov = event.get_payment_providers(cached=True).get(p['provider'])
|
||||
if not pprov:
|
||||
_remove_payment(p['id'])
|
||||
continue
|
||||
|
||||
if not total_includes_payment_fees:
|
||||
fee = pprov.calculate_fee(to_pay)
|
||||
total_remaining += fee
|
||||
to_pay += fee
|
||||
else:
|
||||
fee = Decimal('0.00')
|
||||
|
||||
if p.get('max_value') and to_pay > Decimal(p['max_value']):
|
||||
to_pay = min(to_pay, Decimal(p['max_value']))
|
||||
|
||||
p['payment_amount'] = to_pay
|
||||
p['provider_name'] = pprov.public_name
|
||||
p['pprov'] = pprov
|
||||
p['fee'] = fee
|
||||
total_remaining -= to_pay
|
||||
payments.append(p)
|
||||
return payments
|
||||
|
||||
|
||||
def ensure_payment_is_completed(event, total, cart_session, request):
|
||||
def _remove_payment(payment_id):
|
||||
cart_session['payments'] = [p for p in cart_session['payments'] if p.get('id') != payment_id]
|
||||
|
||||
if not cart_session.get('payments'):
|
||||
raise IncompleteError(_('Please select a payment method to proceed.'))
|
||||
|
||||
selected = current_selected_payments(event, total, cart_session, fail=True, total_includes_payment_fees=True)
|
||||
if sum(p['payment_amount'] for p in selected) != total:
|
||||
raise IncompleteError(_('Please select a payment method to proceed.'))
|
||||
|
||||
if len([p for p in selected if not p['multi_use_supported']]) > 1:
|
||||
raise ImproperlyConfigured('Multiple non-multi-use providers in session, should never happen')
|
||||
|
||||
for p in selected:
|
||||
# TODO: do we need a different is_allowed for storefrontapi?
|
||||
if not p['pprov'] or not p['pprov'].is_enabled or not p['pprov'].is_allowed(request, total=total):
|
||||
_remove_payment(p['id'])
|
||||
if p['payment_amount']:
|
||||
raise IncompleteError(_('Please select a payment method to proceed.'))
|
||||
|
||||
if not p['multi_use_supported'] and not p['pprov'].payment_is_valid_session(request):
|
||||
raise IncompleteError(_('The payment information you entered was incomplete.'))
|
||||
|
||||
|
||||
def current_payments_valid(cart_session, amount):
|
||||
singleton_payments = [p for p in cart_session.get('payments', []) if not p.get('multi_use_supported')]
|
||||
if len(singleton_payments) > 1:
|
||||
return False
|
||||
|
||||
matched = Decimal('0.00')
|
||||
for p in cart_session.get('payments', []):
|
||||
if p.get('min_value') and (amount - matched) < Decimal(p['min_value']):
|
||||
continue
|
||||
if p.get('max_value') and (amount - matched) > Decimal(p['max_value']):
|
||||
matched += Decimal(p['max_value'])
|
||||
else:
|
||||
matched = Decimal('0.00')
|
||||
|
||||
return matched == Decimal('0.00'), amount - matched
|
||||
@@ -0,0 +1,396 @@
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import (
|
||||
Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value,
|
||||
)
|
||||
from django.db.models.lookups import Exact
|
||||
|
||||
from pretix.base.models import (
|
||||
ItemVariation, Quota, SalesChannel, SeatCategoryMapping,
|
||||
)
|
||||
from pretix.base.models.items import (
|
||||
ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
|
||||
)
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.presale.signals import item_description
|
||||
|
||||
|
||||
def item_group_by_category(items):
|
||||
return sorted(
|
||||
[
|
||||
# a group is a tuple of a category and a list of items
|
||||
(cat, [i for i in items if i.category == cat])
|
||||
for cat in set([i.category for i in items])
|
||||
# insert categories into a set for uniqueness
|
||||
# a set is unsorted, so sort again by category
|
||||
],
|
||||
key=lambda group: (group[0].position, group[0].id) if (group[0] is not None and group[0].id is not None) else (0, 0)
|
||||
)
|
||||
|
||||
|
||||
def get_items_for_product_list(event, *, channel: SalesChannel, subevent=None, voucher=None, require_seat=0,
|
||||
base_qs=None, allow_addons=False, allow_cross_sell=False,
|
||||
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
|
||||
ignore_hide_sold_out_for_item_ids=None):
|
||||
base_qs_set = base_qs is not None
|
||||
base_qs = base_qs if base_qs is not None else event.items
|
||||
|
||||
requires_seat = Exists(
|
||||
SeatCategoryMapping.objects.filter(
|
||||
product_id=OuterRef('pk'),
|
||||
subevent=subevent
|
||||
)
|
||||
)
|
||||
if not event.settings.seating_choice:
|
||||
requires_seat = Value(0, output_field=IntegerField())
|
||||
|
||||
variation_q = (
|
||||
Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info')) &
|
||||
Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
|
||||
)
|
||||
if not voucher or not voucher.show_hidden_items:
|
||||
variation_q &= Q(hide_without_voucher=False)
|
||||
|
||||
if memberships is not None:
|
||||
prefetch_membership_types = ['require_membership_types']
|
||||
else:
|
||||
prefetch_membership_types = []
|
||||
|
||||
prefetch_var = Prefetch(
|
||||
'variations',
|
||||
to_attr='available_variations',
|
||||
queryset=ItemVariation.objects.using(settings.DATABASE_REPLICA).annotate(
|
||||
subevent_disabled=Exists(
|
||||
SubEventItemVariation.objects.filter(
|
||||
Q(disabled=True)
|
||||
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=time_machine_now()))
|
||||
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=time_machine_now())),
|
||||
variation_id=OuterRef('pk'),
|
||||
subevent=subevent,
|
||||
)
|
||||
),
|
||||
).filter(
|
||||
variation_q,
|
||||
Q(all_sales_channels=True) | Q(limit_sales_channels=channel),
|
||||
active=True,
|
||||
quotas__isnull=False,
|
||||
subevent_disabled=False
|
||||
).prefetch_related(
|
||||
*prefetch_membership_types,
|
||||
Prefetch('quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
|
||||
subevent=subevent).select_related("subevent"))
|
||||
).distinct()
|
||||
)
|
||||
prefetch_quotas = Prefetch(
|
||||
'quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent).select_related("subevent")
|
||||
)
|
||||
prefetch_bundles = Prefetch(
|
||||
'bundles',
|
||||
queryset=ItemBundle.objects.using(settings.DATABASE_REPLICA).prefetch_related(
|
||||
Prefetch('bundled_item',
|
||||
queryset=event.items.using(settings.DATABASE_REPLICA).select_related(
|
||||
'tax_rule').prefetch_related(
|
||||
Prefetch('quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
|
||||
subevent=subevent)),
|
||||
)),
|
||||
Prefetch('bundled_variation',
|
||||
queryset=ItemVariation.objects.using(
|
||||
settings.DATABASE_REPLICA
|
||||
).select_related('item', 'item__tax_rule').filter(item__event=event).prefetch_related(
|
||||
Prefetch('quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
|
||||
subevent=subevent)),
|
||||
)),
|
||||
)
|
||||
)
|
||||
|
||||
items = base_qs.using(settings.DATABASE_REPLICA).filter_available(
|
||||
channel=channel.identifier, voucher=voucher, allow_addons=allow_addons, allow_cross_sell=allow_cross_sell
|
||||
).select_related(
|
||||
'category', 'tax_rule', # for re-grouping
|
||||
'hidden_if_available',
|
||||
).prefetch_related(
|
||||
*prefetch_membership_types,
|
||||
Prefetch(
|
||||
'hidden_if_item_available',
|
||||
queryset=event.items.annotate(
|
||||
has_variations=Count('variations'),
|
||||
).prefetch_related(
|
||||
prefetch_var,
|
||||
prefetch_quotas,
|
||||
prefetch_bundles,
|
||||
)
|
||||
),
|
||||
prefetch_quotas,
|
||||
prefetch_var,
|
||||
prefetch_bundles,
|
||||
).annotate(
|
||||
quotac=Count('quotas'),
|
||||
has_variations=Count('variations'),
|
||||
subevent_disabled=Exists(
|
||||
SubEventItem.objects.filter(
|
||||
Q(disabled=True)
|
||||
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=time_machine_now()))
|
||||
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=time_machine_now())),
|
||||
item_id=OuterRef('pk'),
|
||||
subevent=subevent,
|
||||
)
|
||||
),
|
||||
mandatory_priced_addons=Exists(
|
||||
ItemAddOn.objects.filter(
|
||||
base_item_id=OuterRef('pk'),
|
||||
min_count__gte=1,
|
||||
price_included=False
|
||||
)
|
||||
),
|
||||
requires_seat=requires_seat,
|
||||
).filter(
|
||||
quotac__gt=0, subevent_disabled=False,
|
||||
).order_by('category__position', 'category_id', 'position', 'name')
|
||||
if require_seat:
|
||||
items = items.filter(requires_seat__gt=0)
|
||||
elif require_seat is not None:
|
||||
items = items.filter(requires_seat=0)
|
||||
|
||||
if filter_items:
|
||||
items = items.filter(pk__in=[a for a in filter_items if a.isdigit()])
|
||||
if filter_categories:
|
||||
items = items.filter(category_id__in=[a for a in filter_categories if a.isdigit()])
|
||||
|
||||
display_add_to_cart = False
|
||||
quota_cache_key = f'item_quota_cache:{subevent.id if subevent else 0}:{channel.identifier}:{bool(require_seat)}'
|
||||
quota_cache = quota_cache or event.cache.get(quota_cache_key) or {}
|
||||
quota_cache_existed = bool(quota_cache)
|
||||
|
||||
if subevent:
|
||||
item_price_override = subevent.item_price_overrides
|
||||
var_price_override = subevent.var_price_overrides
|
||||
else:
|
||||
item_price_override = {}
|
||||
var_price_override = {}
|
||||
|
||||
restrict_vars = set()
|
||||
if voucher and voucher.quota_id:
|
||||
# If a voucher is set to a specific quota, we need to filter out on that level
|
||||
restrict_vars = set(voucher.quota.variations.all())
|
||||
|
||||
quotas_to_compute = []
|
||||
for item in items:
|
||||
assert item.event_id == event.pk
|
||||
item.event = event # save a database query if this is looked up
|
||||
if item.has_variations:
|
||||
for v in item.available_variations:
|
||||
for q in v._subevent_quotas:
|
||||
if q.pk not in quota_cache:
|
||||
quotas_to_compute.append(q)
|
||||
else:
|
||||
for q in item._subevent_quotas:
|
||||
if q.pk not in quota_cache:
|
||||
quotas_to_compute.append(q)
|
||||
|
||||
if quotas_to_compute:
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(*quotas_to_compute)
|
||||
qa.compute()
|
||||
quota_cache.update({q.pk: r for q, r in qa.results.items()})
|
||||
|
||||
for item in items:
|
||||
if voucher and voucher.item_id and voucher.variation_id:
|
||||
# Restrict variations if the voucher only allows one
|
||||
item.available_variations = [v for v in item.available_variations
|
||||
if v.pk == voucher.variation_id]
|
||||
|
||||
if channel.type_instance.unlimited_items_per_order:
|
||||
max_per_order = sys.maxsize
|
||||
else:
|
||||
max_per_order = item.max_per_order or int(event.settings.max_items_per_order)
|
||||
|
||||
if item.hidden_if_available:
|
||||
q = item.hidden_if_available.availability(_cache=quota_cache)
|
||||
if q[0] == Quota.AVAILABILITY_OK:
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
if item.hidden_if_item_available:
|
||||
if item.hidden_if_item_available.has_variations:
|
||||
dependency_available = any(
|
||||
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
|
||||
for var in item.hidden_if_item_available.available_variations
|
||||
)
|
||||
else:
|
||||
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||
dependency_available = q[0] == Quota.AVAILABILITY_OK
|
||||
if dependency_available:
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
if item.require_membership and item.require_membership_hidden:
|
||||
if not memberships or not any([m.membership_type in item.require_membership_types.all() for m in memberships]):
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
item.current_unavailability_reason = item.unavailability_reason(has_voucher=voucher, subevent=subevent)
|
||||
|
||||
item.description = str(item.description)
|
||||
for recv, resp in item_description.send(sender=event, item=item, variation=None, subevent=subevent):
|
||||
if resp:
|
||||
item.description += ("<br/>" if item.description else "") + resp
|
||||
|
||||
if not item.has_variations:
|
||||
item._remove = False
|
||||
if not bool(item._subevent_quotas):
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
|
||||
item.cached_availability = (
|
||||
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
|
||||
)
|
||||
else:
|
||||
item.cached_availability = list(
|
||||
item.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||
)
|
||||
|
||||
if not (
|
||||
ignore_hide_sold_out_for_item_ids and item.pk in ignore_hide_sold_out_for_item_ids
|
||||
) and event.settings.hide_sold_out and item.cached_availability[0] < Quota.AVAILABILITY_RESERVED:
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
item.order_max = min(
|
||||
item.cached_availability[1]
|
||||
if item.cached_availability[1] is not None else sys.maxsize,
|
||||
max_per_order
|
||||
)
|
||||
|
||||
original_price = item_price_override.get(item.pk, item.default_price)
|
||||
voucher_reduced = False
|
||||
if voucher:
|
||||
price = voucher.calculate_price(original_price)
|
||||
voucher_reduced = price < original_price
|
||||
include_bundled = not voucher.all_bundles_included
|
||||
else:
|
||||
price = original_price
|
||||
include_bundled = True
|
||||
|
||||
item.display_price = item.tax(price, currency=event.currency, include_bundled=include_bundled)
|
||||
if item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
|
||||
item.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency, include_bundled=include_bundled)
|
||||
else:
|
||||
item.suggested_price = item.display_price
|
||||
|
||||
if price != original_price:
|
||||
item.original_price = item.tax(original_price, currency=event.currency, include_bundled=True)
|
||||
else:
|
||||
item.original_price = (
|
||||
item.tax(item.original_price, currency=event.currency, include_bundled=True,
|
||||
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
|
||||
if item.original_price else None
|
||||
)
|
||||
if not display_add_to_cart:
|
||||
display_add_to_cart = not item.requires_seat and item.order_max > 0
|
||||
else:
|
||||
for var in item.available_variations:
|
||||
if var.require_membership and var.require_membership_hidden:
|
||||
if not memberships or not any([m.membership_type in var.require_membership_types.all() for m in memberships]):
|
||||
var._remove = True
|
||||
continue
|
||||
|
||||
var.description = str(var.description)
|
||||
for recv, resp in item_description.send(sender=event, item=item, variation=var, subevent=subevent):
|
||||
if resp:
|
||||
var.description += ("<br/>" if var.description else "") + resp
|
||||
|
||||
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
|
||||
var.cached_availability = (
|
||||
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
|
||||
)
|
||||
else:
|
||||
var.cached_availability = list(
|
||||
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||
)
|
||||
|
||||
var.order_max = min(
|
||||
var.cached_availability[1]
|
||||
if var.cached_availability[1] is not None else sys.maxsize,
|
||||
max_per_order
|
||||
)
|
||||
|
||||
original_price = var_price_override.get(var.pk, var.price)
|
||||
voucher_reduced = False
|
||||
if voucher:
|
||||
price = voucher.calculate_price(original_price)
|
||||
voucher_reduced = price < original_price
|
||||
include_bundled = not voucher.all_bundles_included
|
||||
else:
|
||||
price = original_price
|
||||
include_bundled = True
|
||||
|
||||
var.display_price = var.tax(price, currency=event.currency, include_bundled=include_bundled)
|
||||
|
||||
if item.free_price and var.free_price_suggestion is not None and not voucher_reduced:
|
||||
var.suggested_price = item.tax(max(price, var.free_price_suggestion), currency=event.currency,
|
||||
include_bundled=include_bundled)
|
||||
elif item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
|
||||
var.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency,
|
||||
include_bundled=include_bundled)
|
||||
else:
|
||||
var.suggested_price = var.display_price
|
||||
|
||||
if price != original_price:
|
||||
var.original_price = var.tax(original_price, currency=event.currency, include_bundled=True)
|
||||
else:
|
||||
var.original_price = (
|
||||
var.tax(var.original_price or item.original_price, currency=event.currency,
|
||||
include_bundled=True,
|
||||
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
|
||||
) if var.original_price or item.original_price else None
|
||||
|
||||
if not display_add_to_cart:
|
||||
display_add_to_cart = not item.requires_seat and var.order_max > 0
|
||||
|
||||
var.current_unavailability_reason = var.unavailability_reason(has_voucher=voucher, subevent=subevent)
|
||||
|
||||
item.original_price = (
|
||||
item.tax(item.original_price, currency=event.currency, include_bundled=True,
|
||||
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
|
||||
if item.original_price else None
|
||||
)
|
||||
|
||||
item.available_variations = [
|
||||
v for v in item.available_variations if v._subevent_quotas and (
|
||||
not voucher or not voucher.quota_id or v in restrict_vars
|
||||
) and not getattr(v, '_remove', False)
|
||||
]
|
||||
|
||||
if not (ignore_hide_sold_out_for_item_ids and item.pk in ignore_hide_sold_out_for_item_ids) and event.settings.hide_sold_out:
|
||||
item.available_variations = [v for v in item.available_variations
|
||||
if v.cached_availability[0] >= Quota.AVAILABILITY_RESERVED]
|
||||
|
||||
if voucher and voucher.variation_id:
|
||||
item.available_variations = [v for v in item.available_variations
|
||||
if v.pk == voucher.variation_id]
|
||||
|
||||
if len(item.available_variations) > 0:
|
||||
item.min_price = min([v.display_price.net if event.settings.display_net_prices else
|
||||
v.display_price.gross for v in item.available_variations])
|
||||
item.max_price = max([v.display_price.net if event.settings.display_net_prices else
|
||||
v.display_price.gross for v in item.available_variations])
|
||||
item.best_variation_availability = max([v.cached_availability[0] for v in item.available_variations])
|
||||
|
||||
item._remove = not bool(item.available_variations)
|
||||
|
||||
if not quota_cache_existed and not voucher and not allow_addons and not base_qs_set and not filter_items and not filter_categories:
|
||||
event.cache.set(quota_cache_key, quota_cache, 5)
|
||||
items = [item for item in items
|
||||
if (len(item.available_variations) > 0 or not item.has_variations) and not item._remove]
|
||||
return items, display_add_to_cart
|
||||
@@ -131,6 +131,9 @@
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
}
|
||||
.content table td.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
a.button {
|
||||
display: inline-block;
|
||||
@@ -178,6 +181,9 @@
|
||||
pre, pre code {
|
||||
white-space: pre-line;
|
||||
}
|
||||
.text-right, .content table td.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
{% if rtl %}
|
||||
body {
|
||||
@@ -186,6 +192,9 @@
|
||||
.content {
|
||||
text-align: right;
|
||||
}
|
||||
.text-right, .content table td.text-right {
|
||||
text-align: left;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% block addcss %}{% endblock %}
|
||||
|
||||
@@ -52,12 +52,12 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
|
||||
# would make the numbers incorrect. If this branch executes, it's likely a bug in
|
||||
# pretix, but we won't show wrong numbers!
|
||||
if hide_currency:
|
||||
return floatformat(value, 2)
|
||||
return floatformat(value, "2g")
|
||||
else:
|
||||
return '{} {}'.format(arg, floatformat(value, 2))
|
||||
return '{} {}'.format(arg, floatformat(value, "2g"))
|
||||
|
||||
if hide_currency:
|
||||
return floatformat(value, places)
|
||||
return floatformat(value, f"{places}g")
|
||||
|
||||
locale_parts = translation.get_language().split("-", 1)
|
||||
locale = locale_parts[0]
|
||||
@@ -70,7 +70,7 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
|
||||
try:
|
||||
return format_currency(value, arg, locale=locale)
|
||||
except:
|
||||
return '{} {}'.format(arg, floatformat(value, places))
|
||||
return '{} {}'.format(arg, floatformat(value, f"{places}g"))
|
||||
|
||||
|
||||
@register.filter("money_numberfield")
|
||||
|
||||
@@ -305,6 +305,7 @@ def markdown_compile_email(source, allowed_tags=ALLOWED_TAGS, allowed_attributes
|
||||
source,
|
||||
extensions=[
|
||||
'markdown.extensions.sane_lists',
|
||||
'markdown.extensions.tables',
|
||||
EmailNl2BrExtension(),
|
||||
LinkifyAndCleanExtension(
|
||||
linker,
|
||||
|
||||
@@ -67,6 +67,7 @@ class EventSlugBanlistValidator(BanlistValidator):
|
||||
'_global',
|
||||
'__debug__',
|
||||
'api',
|
||||
'storefrontapi',
|
||||
'events',
|
||||
'csp_report',
|
||||
'widget',
|
||||
@@ -91,6 +92,7 @@ class OrganizerSlugBanlistValidator(BanlistValidator):
|
||||
'__debug__',
|
||||
'about',
|
||||
'api',
|
||||
'storefrontapi',
|
||||
'csp_report',
|
||||
'widget',
|
||||
'lead',
|
||||
|
||||
@@ -40,5 +40,5 @@ class PretixControlConfig(AppConfig):
|
||||
label = 'pretixcontrol'
|
||||
|
||||
def ready(self):
|
||||
from .views import dashboards # noqa
|
||||
from . import logdisplay # noqa
|
||||
from .views import dashboards # noqa
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from decimal import Decimal
|
||||
from urllib.parse import urlencode, urlparse
|
||||
from urllib.parse import urlencode
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pycountry
|
||||
@@ -63,6 +63,7 @@ from pretix.base.forms import (
|
||||
)
|
||||
from pretix.base.models import Event, Organizer, TaxRule, Team
|
||||
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
||||
from pretix.base.models.tax import TAX_CODE_LISTS
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
from pretix.base.services.placeholders import FormPlaceholderMixin
|
||||
from pretix.base.settings import (
|
||||
@@ -76,8 +77,10 @@ from pretix.control.forms import (
|
||||
)
|
||||
from pretix.control.forms.widgets import Select2
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain
|
||||
from pretix.multidomain.urlreverse import (
|
||||
build_absolute_uri, get_organizer_domain,
|
||||
)
|
||||
from pretix.plugins.banktransfer.payment import BankTransfer
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
@@ -363,14 +366,9 @@ class EventUpdateForm(I18nModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.change_slug = kwargs.pop('change_slug', False)
|
||||
self.domain = kwargs.pop('domain', False)
|
||||
|
||||
kwargs.setdefault('initial', {})
|
||||
self.instance = kwargs['instance']
|
||||
if self.domain and self.instance:
|
||||
initial_domain = self.instance.domains.first()
|
||||
if initial_domain:
|
||||
kwargs['initial'].setdefault('domain', initial_domain.domainname)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.change_slug:
|
||||
@@ -379,48 +377,54 @@ class EventUpdateForm(I18nModelForm):
|
||||
self.fields['location'].widget.attrs['placeholder'] = _(
|
||||
'Sample Conference Center\nHeidelberg, Germany'
|
||||
)
|
||||
if self.domain:
|
||||
|
||||
try:
|
||||
self.fields['domain'] = forms.CharField(
|
||||
max_length=255,
|
||||
label=_('Custom domain'),
|
||||
label=_('Domain'),
|
||||
initial=self.instance.domain.domainname,
|
||||
required=False,
|
||||
disabled=True,
|
||||
help_text=_('You can configure this in your organizer settings.')
|
||||
)
|
||||
except KnownDomain.DoesNotExist:
|
||||
domain = get_organizer_domain(self.instance.organizer)
|
||||
try:
|
||||
current_domain_assignment = self.instance.alternative_domain_assignment
|
||||
except AlternativeDomainAssignment.DoesNotExist:
|
||||
current_domain_assignment = None
|
||||
self.fields['domain'] = forms.ChoiceField(
|
||||
label=_('Domain'),
|
||||
help_text=_('You can add more domains in your organizer account.'),
|
||||
choices=[('', _('Same as organizer account') + (f" ({domain})" if domain else ""))] + [
|
||||
(d.domainname, d.domainname) for d in self.instance.organizer.domains.filter(mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
|
||||
],
|
||||
initial=current_domain_assignment.domain_id if current_domain_assignment else "",
|
||||
required=False,
|
||||
help_text=_('You need to configure the custom domain in the webserver beforehand.')
|
||||
)
|
||||
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
||||
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
|
||||
'data-inverse-dependency': '<[name$=all_sales_channels]',
|
||||
}, choices=self.fields['limit_sales_channels'].widget.choices)
|
||||
|
||||
def clean_domain(self):
|
||||
d = self.cleaned_data['domain']
|
||||
if d:
|
||||
if d == urlparse(settings.SITE_URL).hostname:
|
||||
raise ValidationError(
|
||||
_('You cannot choose the base domain of this installation.')
|
||||
)
|
||||
if KnownDomain.objects.filter(domainname=d).exclude(event=self.instance.pk).exists():
|
||||
raise ValidationError(
|
||||
_('This domain is already in use for a different event or organizer.')
|
||||
)
|
||||
return d
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit)
|
||||
|
||||
if self.domain:
|
||||
current_domain = instance.domains.first()
|
||||
if self.cleaned_data['domain']:
|
||||
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
|
||||
current_domain.delete()
|
||||
KnownDomain.objects.create(
|
||||
organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain']
|
||||
)
|
||||
elif not current_domain:
|
||||
KnownDomain.objects.create(
|
||||
organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain']
|
||||
)
|
||||
elif current_domain:
|
||||
current_domain.delete()
|
||||
try:
|
||||
current_domain_assignment = instance.alternative_domain_assignment
|
||||
except AlternativeDomainAssignment.DoesNotExist:
|
||||
current_domain_assignment = None
|
||||
if self.cleaned_data['domain'] and not hasattr(instance, 'domain'):
|
||||
domain = self.instance.organizer.domains.get(mode=KnownDomain.MODE_ORG_ALT_DOMAIN, domainname=self.cleaned_data["domain"])
|
||||
AlternativeDomainAssignment.objects.update_or_create(
|
||||
event=instance,
|
||||
defaults={
|
||||
"domain": domain,
|
||||
}
|
||||
)
|
||||
instance.cache.clear()
|
||||
elif current_domain_assignment:
|
||||
current_domain_assignment.delete()
|
||||
instance.cache.clear()
|
||||
|
||||
return instance
|
||||
@@ -1382,7 +1386,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
||||
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
|
||||
|
||||
for k, v in self.base_context.items():
|
||||
self._set_field_placeholders(k, v)
|
||||
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_'))
|
||||
|
||||
for k, v in list(self.fields.items()):
|
||||
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
|
||||
@@ -1501,6 +1505,11 @@ class TaxRuleLineForm(I18nForm):
|
||||
('require_approval', _('Order requires approval')),
|
||||
],
|
||||
)
|
||||
code = forms.ChoiceField(
|
||||
label=_("Tax code"),
|
||||
choices=[("", _("Default tax code")), *TAX_CODE_LISTS],
|
||||
required=False,
|
||||
)
|
||||
rate = forms.DecimalField(
|
||||
label=_('Deviating tax rate'),
|
||||
max_digits=10, decimal_places=2,
|
||||
@@ -1515,6 +1524,43 @@ class TaxRuleLineForm(I18nForm):
|
||||
})
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.parent_form = kwargs.pop("parent_form")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
|
||||
parent_code = self.parent_form.cleaned_data.get("code")
|
||||
parent_rate = self.parent_form.cleaned_data.get("rate")
|
||||
|
||||
code = d.get("code") or parent_code
|
||||
rate = d.get("rate")
|
||||
if rate is None:
|
||||
rate = parent_rate
|
||||
|
||||
if d.get("action") in ("reverse", "no", "block") and d.get("rate"):
|
||||
raise ValidationError(_("A combination of this calculation mode with a non-zero tax rate does not make sense."))
|
||||
|
||||
if d.get("action") == "reverse" and d.get("code") and code != "AE":
|
||||
# Reverse charge but code is not reverse charge -- this is the one case we ignore if the "default code"
|
||||
# is used because it is the one scenario we can auto-fix
|
||||
raise ValidationError(_("This combination of calculation mode and tax code does not make sense."))
|
||||
|
||||
if d.get("action") == "no" and code and code.split("/")[0] in ("S", "AE", "L", "M", "B"):
|
||||
# No VAT but code indicates VAT
|
||||
raise ValidationError(_("This combination of calculation mode and tax code does not make sense."))
|
||||
|
||||
if d.get("action") == "vat" and code and rate != Decimal("0.00") and code.split("/")[0] in ("O", "E", "Z", "G", "K", "AE"):
|
||||
# VAT, but code indicates exempt
|
||||
raise ValidationError(_("A combination of this tax code with a non-zero tax rate does not make sense."))
|
||||
|
||||
if d.get("action") == "vat" and code and rate == Decimal("0.00") and code.split("/")[0] in ("S", "L", "M", "B"):
|
||||
# no VAT, but code indicates non-exempt
|
||||
raise ValidationError(_("A combination of this tax code with a zero tax rate does not make sense."))
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class I18nBaseFormSet(I18nFormSetMixin, forms.BaseFormSet):
|
||||
# compatibility shim for django-i18nfield library
|
||||
@@ -1526,8 +1572,16 @@ class I18nBaseFormSet(I18nFormSetMixin, forms.BaseFormSet):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class BaseTaxRuleLineFormSet(I18nBaseFormSet):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.parent_form = kwargs.pop('parent_form')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_kwargs['parent_form'] = self.parent_form
|
||||
|
||||
|
||||
TaxRuleLineFormSet = formset_factory(
|
||||
TaxRuleLineForm, formset=I18nBaseFormSet,
|
||||
TaxRuleLineForm, formset=BaseTaxRuleLineFormSet,
|
||||
can_order=True, can_delete=True, extra=0
|
||||
)
|
||||
|
||||
@@ -1535,7 +1589,16 @@ TaxRuleLineFormSet = formset_factory(
|
||||
class TaxRuleForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = TaxRule
|
||||
fields = ['name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name', 'keep_gross_if_rate_changes']
|
||||
fields = [
|
||||
'name',
|
||||
'rate',
|
||||
'price_includes_tax',
|
||||
'code',
|
||||
'eu_reverse_charge',
|
||||
'home_country',
|
||||
'internal_name',
|
||||
'keep_gross_if_rate_changes'
|
||||
]
|
||||
|
||||
|
||||
class WidgetCodeForm(forms.Form):
|
||||
|
||||
@@ -490,7 +490,9 @@ class OrderPositionChangeForm(forms.Form):
|
||||
)
|
||||
operation_secret = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Generate a new secret')
|
||||
label=_('Generate a new secret'),
|
||||
help_text=_('This affects both the ticket secret (often used as a QR code) as well as the link used to '
|
||||
'individually access the ticket.')
|
||||
)
|
||||
operation_cancel = forms.BooleanField(
|
||||
required=False,
|
||||
|
||||
@@ -133,63 +133,108 @@ class OrganizerDeleteForm(forms.Form):
|
||||
class OrganizerUpdateForm(OrganizerForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.domain = kwargs.pop('domain', False)
|
||||
self.change_slug = kwargs.pop('change_slug', False)
|
||||
kwargs.setdefault('initial', {})
|
||||
self.instance = kwargs['instance']
|
||||
if self.domain and self.instance:
|
||||
initial_domain = self.instance.domains.filter(event__isnull=True).first()
|
||||
if initial_domain:
|
||||
kwargs['initial'].setdefault('domain', initial_domain.domainname)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.change_slug:
|
||||
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
|
||||
if self.domain:
|
||||
self.fields['domain'] = forms.CharField(
|
||||
max_length=255,
|
||||
label=_('Custom domain'),
|
||||
required=False,
|
||||
help_text=_('You need to configure the custom domain in the webserver beforehand.')
|
||||
)
|
||||
|
||||
def clean_domain(self):
|
||||
d = self.cleaned_data['domain']
|
||||
if d:
|
||||
if d == urlparse(settings.SITE_URL).hostname:
|
||||
raise ValidationError(
|
||||
_('You cannot choose the base domain of this installation.')
|
||||
)
|
||||
if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.pk,
|
||||
event__isnull=True).exists():
|
||||
raise ValidationError(
|
||||
_('This domain is already in use for a different event or organizer.')
|
||||
)
|
||||
return d
|
||||
|
||||
def clean_slug(self):
|
||||
if self.change_slug:
|
||||
return self.cleaned_data['slug']
|
||||
return self.instance.slug
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit)
|
||||
|
||||
if self.domain:
|
||||
current_domain = instance.domains.filter(event__isnull=True).first()
|
||||
if self.cleaned_data['domain']:
|
||||
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
|
||||
current_domain.delete()
|
||||
KnownDomain.objects.create(organizer=instance, domainname=self.cleaned_data['domain'])
|
||||
elif not current_domain:
|
||||
KnownDomain.objects.create(organizer=instance, domainname=self.cleaned_data['domain'])
|
||||
elif current_domain:
|
||||
current_domain.delete()
|
||||
instance.cache.clear()
|
||||
for ev in instance.events.all():
|
||||
ev.cache.clear()
|
||||
class KnownDomainForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = KnownDomain
|
||||
fields = ["domainname", "mode", "event"]
|
||||
field_classes = {
|
||||
"event": SafeModelChoiceField,
|
||||
}
|
||||
|
||||
return instance
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.organizer = kwargs.pop('organizer')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["event"].queryset = self.organizer.events.all()
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields["domainname"].widget.attrs['readonly'] = 'readonly'
|
||||
|
||||
def clean_domainname(self):
|
||||
if self.instance and self.instance.pk:
|
||||
return self.instance.domainname
|
||||
d = self.cleaned_data['domainname']
|
||||
if d:
|
||||
if d == urlparse(settings.SITE_URL).hostname:
|
||||
raise ValidationError(
|
||||
_('You cannot choose the base domain of this installation.')
|
||||
)
|
||||
if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.organizer).exists():
|
||||
raise ValidationError(
|
||||
_('This domain is already in use for a different event or organizer.')
|
||||
)
|
||||
return d
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
|
||||
if d["mode"] == KnownDomain.MODE_ORG_DOMAIN and d["event"]:
|
||||
raise ValidationError(
|
||||
_("Do not choose an event for this mode.")
|
||||
)
|
||||
|
||||
if d["mode"] == KnownDomain.MODE_ORG_ALT_DOMAIN and d["event"]:
|
||||
raise ValidationError(
|
||||
_("Do not choose an event for this mode. You can assign events to this domain in event settings.")
|
||||
)
|
||||
|
||||
if d["mode"] == KnownDomain.MODE_EVENT_DOMAIN and not d["event"]:
|
||||
raise ValidationError(
|
||||
_("You need to choose an event.")
|
||||
)
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class BaseKnownDomainFormSet(forms.BaseInlineFormSet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.organizer = kwargs.pop('organizer')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _construct_form(self, i, **kwargs):
|
||||
kwargs['organizer'] = self.organizer
|
||||
return super()._construct_form(i, **kwargs)
|
||||
|
||||
@property
|
||||
def empty_form(self):
|
||||
form = self.form(
|
||||
auto_id=self.auto_id,
|
||||
prefix=self.add_prefix('__prefix__'),
|
||||
empty_permitted=True,
|
||||
use_required_attribute=False,
|
||||
organizer=self.organizer,
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
data = [f.cleaned_data for f in self.forms]
|
||||
|
||||
if len([d for d in data if d.get("mode") == KnownDomain.MODE_ORG_DOMAIN and not d.get("DELETE")]) > 1:
|
||||
raise ValidationError(_("You may set only one organizer domain."))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
KnownDomainFormset = inlineformset_factory(
|
||||
Organizer, KnownDomain,
|
||||
KnownDomainForm,
|
||||
formset=BaseKnownDomainFormSet,
|
||||
can_order=False, can_delete=True, extra=0
|
||||
)
|
||||
|
||||
|
||||
class SafeOrderPositionChoiceField(forms.ModelChoiceField):
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.slug layout="control" %}
|
||||
{% if form.domain %}
|
||||
{% bootstrap_field form.domain layout="control" %}
|
||||
{% bootstrap_field form.domain layout="horizontal" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.date_from layout="control" %}
|
||||
{% bootstrap_field form.date_to layout="control" %}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.internal_name layout="control" %}
|
||||
{% bootstrap_field form.rate addon_after="%" layout="control" %}
|
||||
{% bootstrap_field form.code layout="control" %}
|
||||
{% bootstrap_field form.price_includes_tax layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
@@ -52,6 +53,18 @@
|
||||
{% trans "All of these rules will only apply if an invoice address is set." %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-md-3 col-lg-3">
|
||||
<strong>{% trans "Condition" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-3">
|
||||
<strong>{% trans "Calculation" %}</strong>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-4 col-sm-offset-6 col-md-offset-0">
|
||||
<strong>{% trans "Reason" %}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="formset tax-rules-formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
@@ -65,14 +78,17 @@
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-3">
|
||||
{% bootstrap_field formset.empty_form.country layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-4">
|
||||
{% bootstrap_field formset.empty_form.address_type layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-3">
|
||||
{% bootstrap_field formset.empty_form.action layout='inline' form_group_class="" %}
|
||||
{% bootstrap_field formset.empty_form.rate layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-2 text-right flip">
|
||||
<div class="col-sm-6 col-md-3 col-lg-4 col-sm-offset-6 col-md-offset-0">
|
||||
{% bootstrap_field formset.empty_form.code layout='inline' form_group_class="" %}
|
||||
{% bootstrap_field formset.empty_form.invoice_text layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-2 col-sm-offset-6 col-md-offset-0 text-right flip">
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
@@ -80,12 +96,6 @@
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-4 col-md-offset-3">
|
||||
{% bootstrap_field formset.empty_form.invoice_text layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-3">
|
||||
{% bootstrap_field formset.empty_form.rate layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
@@ -100,14 +110,17 @@
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-3">
|
||||
{% bootstrap_field form.country layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-4">
|
||||
{% bootstrap_field form.address_type layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-3">
|
||||
{% bootstrap_field form.action layout='inline' form_group_class="" %}
|
||||
{% bootstrap_field form.rate layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-2 text-right flip">
|
||||
<div class="col-sm-6 col-md-3 col-lg-4 col-sm-offset-6 col-md-offset-0">
|
||||
{% bootstrap_field form.code layout='inline' form_group_class="" %}
|
||||
{% bootstrap_field form.invoice_text layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-2 col-sm-offset-6 col-md-offset-0 text-right flip">
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
@@ -115,12 +128,6 @@
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-4 col-md-offset-3">
|
||||
{% bootstrap_field form.invoice_text layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-3 col-lg-3">
|
||||
{% bootstrap_field form.rate layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -220,7 +220,6 @@
|
||||
{% endif %}
|
||||
{% bootstrap_field formset.empty_form.available_from visibility_field=formset.empty_form.available_from_mode layout="control_with_visibility" %}
|
||||
{% bootstrap_field formset.empty_form.available_until visibility_field=formset.empty_form.available_until_mode layout="control_with_visibility" %}
|
||||
{% bootstrap_field formset.empty_form.available_until layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.all_sales_channels layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.limit_sales_channels layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.hide_without_voucher layout="control" %}
|
||||
|
||||
@@ -202,6 +202,12 @@
|
||||
+ {{ position.tax_rate }}%)
|
||||
</small>
|
||||
{% endif %}
|
||||
{% if position.tax_code %}
|
||||
<br>
|
||||
<small>
|
||||
{{ position.get_tax_code_display }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-4 field-container">
|
||||
{% bootstrap_field position.form.price addon_after=request.event.currency layout='inline' %}
|
||||
@@ -420,6 +426,12 @@
|
||||
+ {{ fee.tax_rate }}%)
|
||||
</small>
|
||||
{% endif %}
|
||||
{% if fee.tax_code %}
|
||||
<br>
|
||||
<small>
|
||||
{{ fee.get_tax_code_display }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-4 field-container">
|
||||
{% bootstrap_field fee.form.value addon_after=request.event.currency layout='inline' %}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "Product" %}</th>
|
||||
<th class="text-right flip">{% trans "Tax rate" %}</th>
|
||||
<th>{% trans "Tax code" %}</th>
|
||||
<th class="text-right flip">{% trans "Quantity" %}</th>
|
||||
<th class="text-right flip">{% trans "Single price" %}</th>
|
||||
<th class="text-right flip">{% trans "Total tax value" %}</th>
|
||||
@@ -52,6 +53,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">{{ t.tax_rate }} %</td>
|
||||
<td>{{ t.get_tax_code_display }}</td>
|
||||
<td class="text-right flip">{{ t.count }} ×</td>
|
||||
<td class="text-right flip">{{ t.price|money:request.event.currency }}</td>
|
||||
<td class="text-right flip">{{ t.full_tax_value|money:request.event.currency }}</td>
|
||||
@@ -64,8 +66,8 @@
|
||||
<td>
|
||||
<strong>{% trans "Sum" %}</strong>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right flip">
|
||||
<strong>
|
||||
|
||||
@@ -294,6 +294,71 @@
|
||||
<legend>{% trans "Invoices" %}</legend>
|
||||
{% bootstrap_field sform.invoice_regenerate_allowed layout="control" %}
|
||||
</fieldset>
|
||||
{% if domain_formset %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Domains" %}</legend>
|
||||
<div class="alert alert-warning">
|
||||
{% trans "This dialog is intended for advanced users." %}
|
||||
{% trans "The domain needs to be configured on your webserver before it can be used here." %}
|
||||
</div>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ domain_formset.prefix }}">
|
||||
{{ domain_formset.management_form }}
|
||||
{% bootstrap_formset_errors domain_formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in domain_formset %}
|
||||
<div class="row formset-row" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.domainname layout='' form_group_class="" %}
|
||||
{% bootstrap_form_errors form %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% bootstrap_field form.mode layout='' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% bootstrap_field form.event layout='' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<label aria-hidden="true"> </label><br>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="row formset-row" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ domain_formset.empty_form.id }}
|
||||
{% bootstrap_field domain_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field domain_formset.empty_form.domainname layout='' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% bootstrap_field domain_formset.empty_form.mode layout='' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% bootstrap_field domain_formset.empty_form.event layout='' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<label aria-hidden="true"> </label><br>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add domain" %}</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
@@ -38,6 +38,7 @@ from zoneinfo import ZoneInfo
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.humanize.templatetags.humanize import intcomma
|
||||
from django.db.models import (
|
||||
Count, IntegerField, Max, Min, OuterRef, Prefetch, Q, Subquery, Sum,
|
||||
)
|
||||
@@ -47,7 +48,6 @@ from django.http import JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.utils import formats
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import now
|
||||
@@ -67,6 +67,7 @@ from pretix.control.signals import (
|
||||
from pretix.helpers.daterange import daterange
|
||||
|
||||
from ...base.models.orders import CancellationRequest
|
||||
from ...base.templatetags.money import money_filter
|
||||
from ..logdisplay import OVERVIEW_BANLIST
|
||||
|
||||
NUM_WIDGET = '<div class="numwidget"><span class="num">{num}</span><span class="text">{text}</span></div>'
|
||||
@@ -111,7 +112,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
|
||||
return [
|
||||
{
|
||||
'content': None if lazy else NUM_WIDGET.format(num=tickc, text=_('Attendees (ordered)')),
|
||||
'content': None if lazy else NUM_WIDGET.format(num=intcomma(tickc), text=_('Attendees (ordered)')),
|
||||
'lazy': 'attendees-ordered',
|
||||
'display_size': 'small',
|
||||
'priority': 100,
|
||||
@@ -121,7 +122,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
||||
},
|
||||
{
|
||||
'content': None if lazy else NUM_WIDGET.format(num=paidc, text=_('Attendees (paid)')),
|
||||
'content': None if lazy else NUM_WIDGET.format(num=intcomma(paidc), text=_('Attendees (paid)')),
|
||||
'lazy': 'attendees-paid',
|
||||
'display_size': 'small',
|
||||
'priority': 100,
|
||||
@@ -132,7 +133,9 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
},
|
||||
{
|
||||
'content': None if lazy else NUM_WIDGET.format(
|
||||
num=formats.localize(round_decimal(rev, sender.currency)), text=_('Total revenue ({currency})').format(currency=sender.currency)),
|
||||
num=money_filter(round_decimal(rev, sender.currency), sender.currency, hide_currency=True),
|
||||
text=_('Total revenue ({currency})').format(currency=sender.currency)
|
||||
),
|
||||
'lazy': 'total-revenue',
|
||||
'display_size': 'small',
|
||||
'priority': 100,
|
||||
@@ -207,7 +210,7 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
|
||||
widgets.append({
|
||||
'content': None if lazy else NUM_WIDGET.format(
|
||||
num=str(happy), text=_('available to give to people on waiting list')
|
||||
num=intcomma(happy), text=_('available to give to people on waiting list')
|
||||
),
|
||||
'lazy': 'waitinglist-avail',
|
||||
'priority': 50,
|
||||
@@ -217,7 +220,7 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
})
|
||||
})
|
||||
widgets.append({
|
||||
'content': None if lazy else NUM_WIDGET.format(num=str(wles.count()), text=_('total waiting list length')),
|
||||
'content': None if lazy else NUM_WIDGET.format(num=intcomma(wles.count()), text=_('total waiting list length')),
|
||||
'lazy': 'waitinglist-length',
|
||||
'display_size': 'small',
|
||||
'priority': 50,
|
||||
@@ -245,7 +248,7 @@ def quota_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
status, left = qa.results[q] if q in qa.results else q.availability(allow_cache=True)
|
||||
widgets.append({
|
||||
'content': None if lazy else NUM_WIDGET.format(
|
||||
num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e',
|
||||
num='{}/{}'.format(intcomma(left), intcomma(q.size)) if q.size is not None else '\u221e',
|
||||
text=_('{quota} left').format(quota=escape(q.name))
|
||||
),
|
||||
'lazy': 'quota-{}'.format(q.pk),
|
||||
@@ -297,7 +300,7 @@ def checkin_widget(sender, subevent=None, lazy=False, **kwargs):
|
||||
for cl in qs:
|
||||
widgets.append({
|
||||
'content': None if lazy else NUM_WIDGET.format(
|
||||
num='{}/{}'.format(cl.inside_count, cl.position_count),
|
||||
num='{}/{}'.format(intcomma(cl.inside_count), intcomma(cl.position_count)),
|
||||
text=_('Present – {list}').format(list=escape(cl.name))
|
||||
),
|
||||
'lazy': 'checkin-{}'.format(cl.pk),
|
||||
|
||||
@@ -62,7 +62,7 @@ from django.http import (
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import escape
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
|
||||
@@ -100,9 +100,12 @@ from ...base.models.items import (
|
||||
Item, ItemCategory, ItemMetaProperty, Question, Quota,
|
||||
)
|
||||
from ...base.services.mail import prefix_subject
|
||||
from ...base.services.placeholders import get_sample_context
|
||||
from ...base.settings import LazyI18nStringList
|
||||
from ...helpers.compat import CompatDeleteView
|
||||
from ...helpers.format import format_map
|
||||
from ...helpers.format import (
|
||||
PlainHtmlAlternativeString, SafeFormatter, format_map,
|
||||
)
|
||||
from ..logdisplay import OVERVIEW_BANLIST
|
||||
from . import CreateView, PaginationMixin, UpdateView
|
||||
|
||||
@@ -239,7 +242,6 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
|
||||
kwargs = super().get_form_kwargs()
|
||||
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
||||
kwargs['change_slug'] = True
|
||||
kwargs['domain'] = True
|
||||
return kwargs
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -717,20 +719,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
|
||||
# get all supported placeholders with dummy values
|
||||
def placeholders(self, item):
|
||||
ctx = {}
|
||||
for p in get_available_placeholders(self.request.event, MailSettingsForm.base_context[item]).values():
|
||||
s = str(p.render_sample(self.request.event))
|
||||
if s.strip().startswith('* '):
|
||||
ctx[p.identifier] = '<div class="placeholder" title="{}">{}</div>'.format(
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
markdown_compile_email(s)
|
||||
)
|
||||
else:
|
||||
ctx[p.identifier] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
escape(s)
|
||||
)
|
||||
return ctx
|
||||
return get_sample_context(self.request.event, MailSettingsForm.base_context[item])
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
preview_item = request.POST.get('item', '')
|
||||
@@ -752,9 +741,15 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
bleach.clean(v), self.placeholders(preview_item), raise_on_missing=True
|
||||
), highlight=True)
|
||||
else:
|
||||
msgs[self.supported_locale[idx]] = markdown_compile_email(
|
||||
format_map(v, self.placeholders(preview_item), raise_on_missing=True)
|
||||
placeholders = self.placeholders(preview_item)
|
||||
msgs[self.supported_locale[idx]] = format_map(
|
||||
markdown_compile_email(
|
||||
format_map(v, placeholders, raise_on_missing=True)
|
||||
),
|
||||
placeholders,
|
||||
mode=SafeFormatter.MODE_RICH_TO_HTML,
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
|
||||
PlaceholderValidator.error_message)
|
||||
@@ -777,13 +772,18 @@ class MailSettingsRendererPreview(MailSettingsPreview):
|
||||
# get all supported placeholders with dummy values
|
||||
def placeholders(self, item):
|
||||
ctx = {}
|
||||
for p in get_available_placeholders(self.request.event, MailSettingsForm.base_context[item]).values():
|
||||
ctx[p.identifier] = escape(str(p.render_sample(self.request.event)))
|
||||
for p in get_available_placeholders(self.request.event, MailSettingsForm.base_context[item], rich=True).values():
|
||||
sample = p.render_sample(self.request.event)
|
||||
if isinstance(sample, PlainHtmlAlternativeString):
|
||||
ctx[p.identifier] = sample
|
||||
else:
|
||||
ctx[p.identifier] = conditional_escape(sample)
|
||||
return ctx
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
v = str(request.event.settings.mail_text_order_placed)
|
||||
v = format_map(v, self.placeholders('mail_text_order_placed'))
|
||||
context = self.placeholders('mail_text_order_placed')
|
||||
v = format_map(v, context)
|
||||
renderers = request.event.get_html_mail_renderers()
|
||||
if request.GET.get('renderer') in renderers:
|
||||
with rolledback_transaction():
|
||||
@@ -801,7 +801,8 @@ class MailSettingsRendererPreview(MailSettingsPreview):
|
||||
str(request.event.settings.mail_text_signature),
|
||||
gettext('Your order: %(code)s') % {'code': order.code},
|
||||
order,
|
||||
position=None
|
||||
position=None,
|
||||
context=context,
|
||||
)
|
||||
r = HttpResponse(v, content_type='text/html')
|
||||
r._csp_ignore = True
|
||||
@@ -1196,17 +1197,22 @@ class TaxCreate(EventSettingsViewMixin, EventPermissionRequiredMixin, CreateView
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = None
|
||||
form = self.get_form()
|
||||
form = self.form
|
||||
if form.is_valid() and self.formset.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
@cached_property
|
||||
def form(self):
|
||||
return self.get_form()
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
return TaxRuleLineFormSet(
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
event=self.request.event,
|
||||
parent_form=self.form,
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -1247,17 +1253,22 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object(self.get_queryset())
|
||||
form = self.get_form()
|
||||
form = self.form
|
||||
if form.is_valid() and self.formset.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
@cached_property
|
||||
def form(self):
|
||||
return self.get_form()
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
return TaxRuleLineFormSet(
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
event=self.request.event,
|
||||
parent_form=self.form,
|
||||
initial=json.loads(self.object.custom_rules) if self.object.custom_rules else []
|
||||
)
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ from pretix.control.signals import order_search_forms
|
||||
from pretix.control.views import PaginationMixin
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.compat import CompatDeleteView
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
from pretix.helpers.safedownload import check_token
|
||||
from pretix.presale.signals import question_form_fields
|
||||
|
||||
@@ -2241,6 +2241,8 @@ class OrderContactChange(OrderView):
|
||||
changed = True
|
||||
self.order.secret = generate_secret()
|
||||
for op in self.order.all_positions.all():
|
||||
op.web_secret = generate_secret()
|
||||
op.save(update_fields=["web_secret"])
|
||||
assign_ticket_secret(
|
||||
self.request.event, position=op, force_invalidate=True, save=True
|
||||
)
|
||||
@@ -2351,7 +2353,7 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
|
||||
'subject': mark_safe(_('Subject: {subject}').format(
|
||||
subject=prefix_subject(order.event, escape(email_subject), highlight=True)
|
||||
)),
|
||||
'html': markdown_compile_email(email_content)
|
||||
'html': format_map(markdown_compile_email(email_content), email_context, mode=SafeFormatter.MODE_RICH_TO_HTML)
|
||||
}
|
||||
return self.get(self.request, *self.args, **self.kwargs)
|
||||
else:
|
||||
|
||||
@@ -104,11 +104,11 @@ from pretix.control.forms.organizer import (
|
||||
CustomerCreateForm, CustomerUpdateForm, DeviceBulkEditForm, DeviceForm,
|
||||
EventMetaPropertyAllowedValueFormSet, EventMetaPropertyForm, GateForm,
|
||||
GiftCardAcceptanceInviteForm, GiftCardCreateForm, GiftCardUpdateForm,
|
||||
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
|
||||
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm,
|
||||
OrganizerSettingsForm, OrganizerUpdateForm, ReusableMediumCreateForm,
|
||||
ReusableMediumUpdateForm, SalesChannelForm, SSOClientForm, SSOProviderForm,
|
||||
TeamForm, WebHookForm,
|
||||
KnownDomainFormset, MailSettingsForm, MembershipTypeForm,
|
||||
MembershipUpdateForm, OrganizerDeleteForm, OrganizerFooterLinkFormset,
|
||||
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm,
|
||||
ReusableMediumCreateForm, ReusableMediumUpdateForm, SalesChannelForm,
|
||||
SSOClientForm, SSOProviderForm, TeamForm, WebHookForm,
|
||||
)
|
||||
from pretix.control.forms.rrule import RRuleForm
|
||||
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
||||
@@ -122,7 +122,7 @@ from pretix.control.views.mailsetup import MailSettingsSetupView
|
||||
from pretix.helpers import OF_SELF, GroupConcat
|
||||
from pretix.helpers.compat import CompatDeleteView
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.presale.forms.customer import TokenGenerator
|
||||
@@ -357,9 +357,10 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
|
||||
highlight=True,
|
||||
)
|
||||
else:
|
||||
msgs[self.supported_locale[idx]] = markdown_compile_email(
|
||||
format_map(v, self.placeholders(preview_item))
|
||||
)
|
||||
placeholders = self.placeholders(preview_item)
|
||||
msgs[self.supported_locale[idx]] = format_map(markdown_compile_email(
|
||||
format_map(v, placeholders)
|
||||
), placeholders, mode=SafeFormatter.MODE_RICH_TO_HTML)
|
||||
|
||||
return JsonResponse({
|
||||
'item': preview_item,
|
||||
@@ -447,6 +448,10 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
def get_object(self, queryset=None) -> Organizer:
|
||||
return self.object
|
||||
|
||||
@cached_property
|
||||
def domain_config(self):
|
||||
return self.request.user.has_active_staff_session(self.request.session.session_key)
|
||||
|
||||
@cached_property
|
||||
def sform(self):
|
||||
return OrganizerSettingsForm(
|
||||
@@ -461,6 +466,8 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['sform'] = self.sform
|
||||
context['footer_links_formset'] = self.footer_links_formset
|
||||
if self.domain_config:
|
||||
context['domain_formset'] = self.domain_formset
|
||||
return context
|
||||
|
||||
@transaction.atomic
|
||||
@@ -483,6 +490,8 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
self.request.organizer.log_action('pretix.organizer.footerlinks.changed', user=self.request.user, data={
|
||||
'data': self.footer_links_formset.cleaned_data
|
||||
})
|
||||
if self.domain_config and self.domain_formset.has_changed():
|
||||
self._save_domain_config()
|
||||
if form.has_changed():
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.changed',
|
||||
@@ -493,10 +502,22 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def _save_domain_config(self):
|
||||
for form in self.domain_formset.initial_forms:
|
||||
if form.instance.pk and form.has_changed():
|
||||
self.object.domains.get(pk=form.instance.pk).log_delete(self.request.user)
|
||||
self.domain_formset.save()
|
||||
for new_obj in self.domain_formset.new_objects:
|
||||
new_obj.log_create(self.request.user)
|
||||
for ch_obj, form in self.domain_formset.changed_objects:
|
||||
ch_obj.log_create(self.request.user)
|
||||
self.request.organizer.cache.clear()
|
||||
for ev in self.request.organizer.events.all():
|
||||
ev.cache.clear()
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
||||
kwargs['domain'] = True
|
||||
kwargs['change_slug'] = True
|
||||
return kwargs
|
||||
|
||||
@@ -508,7 +529,7 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid() and self.sform.is_valid() and self.footer_links_formset.is_valid():
|
||||
if form.is_valid() and self.sform.is_valid() and self.footer_links_formset.is_valid() and (not self.domain_config or self.domain_formset.is_valid()):
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
@@ -519,6 +540,11 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
organizer=self.object,
|
||||
prefix="footer-links", instance=self.object)
|
||||
|
||||
@cached_property
|
||||
def domain_formset(self):
|
||||
return KnownDomainFormset(self.request.POST if self.request.method == "POST" else None, prefix="domains",
|
||||
instance=self.object, organizer=self.object)
|
||||
|
||||
def save_footer_links_formset(self, obj):
|
||||
self.footer_links_formset.save()
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ from django.http import (
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import escape, format_html
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -59,12 +59,12 @@ from django.views.generic import (
|
||||
)
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.email import get_available_placeholders
|
||||
from pretix.base.models import (
|
||||
CartPosition, LogEntry, Voucher, WaitingListEntry,
|
||||
)
|
||||
from pretix.base.models.vouchers import generate_codes
|
||||
from pretix.base.services.mail import prefix_subject
|
||||
from pretix.base.services.placeholders import get_sample_context
|
||||
from pretix.base.services.vouchers import vouchers_send
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.base.views.tasks import AsyncFormView
|
||||
@@ -74,7 +74,7 @@ from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.signals import voucher_form_class
|
||||
from pretix.control.views import PaginationMixin
|
||||
from pretix.helpers.compat import CompatDeleteView
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
from pretix.helpers.models import modelcopy
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
@@ -549,22 +549,10 @@ class VoucherBulkMailPreview(EventPermissionRequiredMixin, View):
|
||||
|
||||
# get all supported placeholders with dummy values
|
||||
def placeholders(self, item):
|
||||
ctx = {}
|
||||
base_ctx = ['event', 'name']
|
||||
if item == 'send_message':
|
||||
base_ctx += ['voucher_list']
|
||||
for p in get_available_placeholders(self.request.event, base_ctx).values():
|
||||
s = str(p.render_sample(self.request.event))
|
||||
if s.strip().startswith('* ') or s.startswith(' '):
|
||||
ctx[p.identifier] = '<div class="placeholder" title="{}">{}</div>'.format(
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
markdown_compile_email(s)
|
||||
)
|
||||
else:
|
||||
ctx[p.identifier] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
escape(s)
|
||||
)
|
||||
ctx = get_sample_context(self.request.event, base_ctx)
|
||||
return self.SafeDict(ctx)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -579,9 +567,10 @@ class VoucherBulkMailPreview(EventPermissionRequiredMixin, View):
|
||||
highlight=True
|
||||
)
|
||||
else:
|
||||
msgs["all"] = markdown_compile_email(
|
||||
format_map(request.POST.get(preview_item), self.placeholders(preview_item))
|
||||
)
|
||||
placeholders = self.placeholders(preview_item)
|
||||
msgs["all"] = format_map(markdown_compile_email(
|
||||
format_map(request.POST.get(preview_item), placeholders)
|
||||
), placeholders, mode=SafeFormatter.MODE_RICH_TO_HTML)
|
||||
|
||||
return JsonResponse({
|
||||
'item': preview_item,
|
||||
|
||||
@@ -25,14 +25,29 @@ from string import Formatter
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlainHtmlAlternativeString:
|
||||
def __init__(self, plain, html, is_block=False):
|
||||
self.plain = plain
|
||||
self.html = html
|
||||
self.is_block = is_block
|
||||
|
||||
def __repr__(self):
|
||||
return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')"
|
||||
|
||||
|
||||
class SafeFormatter(Formatter):
|
||||
"""
|
||||
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
|
||||
(b) does not allow any unwanted shenanigans like attribute access or format specifiers.
|
||||
"""
|
||||
def __init__(self, context, raise_on_missing=False):
|
||||
MODE_IGNORE_RICH = 0
|
||||
MODE_RICH_TO_PLAIN = 1
|
||||
MODE_RICH_TO_HTML = 2
|
||||
|
||||
def __init__(self, context, raise_on_missing=False, mode=MODE_IGNORE_RICH):
|
||||
self.context = context
|
||||
self.raise_on_missing = raise_on_missing
|
||||
self.mode = mode
|
||||
|
||||
def get_field(self, field_name, args, kwargs):
|
||||
return self.get_value(field_name, args, kwargs), field_name
|
||||
@@ -40,14 +55,22 @@ class SafeFormatter(Formatter):
|
||||
def get_value(self, key, args, kwargs):
|
||||
if not self.raise_on_missing and key not in self.context:
|
||||
return '{' + str(key) + '}'
|
||||
return self.context[key]
|
||||
r = self.context[key]
|
||||
if isinstance(r, PlainHtmlAlternativeString):
|
||||
if self.mode == self.MODE_IGNORE_RICH:
|
||||
return '{' + str(key) + '}'
|
||||
elif self.mode == self.MODE_RICH_TO_PLAIN:
|
||||
return r.plain
|
||||
elif self.mode == self.MODE_RICH_TO_HTML:
|
||||
return r.html
|
||||
return r
|
||||
|
||||
def format_field(self, value, format_spec):
|
||||
# Ignore format _spec
|
||||
# Ignore format_spec
|
||||
return super().format_field(value, '')
|
||||
|
||||
|
||||
def format_map(template, context, raise_on_missing=False):
|
||||
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_IGNORE_RICH):
|
||||
if not isinstance(template, str):
|
||||
template = str(template)
|
||||
return SafeFormatter(context, raise_on_missing).format(template)
|
||||
return SafeFormatter(context, raise_on_missing, mode=mode).format(template)
|
||||
|
||||
+2119
-1846
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: 2024-11-08 13:45+0000\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+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"
|
||||
@@ -622,40 +622,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -705,20 +705,20 @@ msgstr ""
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
msgid "required"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+2234
-1854
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: 2024-11-08 13:45+0000\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+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/"
|
||||
@@ -656,40 +656,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr "تباين اللون سيئ للخلفية البيضاء، الرجاء اختيار لون غامق."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr "البحث في الاستفسارات"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr "الكل"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr "لا شيء"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr "المختارة فقط"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr "قم باستخدم اسم مختلف داخليا"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr "اضغط لاغلاق الصفحة"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "لم تقم بحفظ التعديلات!"
|
||||
|
||||
@@ -753,20 +753,20 @@ msgstr "ستسترد %(currency)%(amount)"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
msgid "required"
|
||||
msgstr "مطلوب"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr "المنطقة الزمنية:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr "التوقيت المحلي:"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2207
-1855
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: 2024-11-08 13:45+0000\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+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-"
|
||||
@@ -623,40 +623,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -710,22 +710,22 @@ msgstr ""
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "required"
|
||||
msgstr "Cistella expirada"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+2206
-1839
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: 2024-11-08 13:45+0000\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+0000\n"
|
||||
"PO-Revision-Date: 2023-09-15 06:00+0000\n"
|
||||
"Last-Translator: Michael <michael.happl@gmx.at>\n"
|
||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -650,40 +650,40 @@ msgstr ""
|
||||
"Tato barva je pro text na bílém pozadí špatně kontrastní, zvolte prosím "
|
||||
"tmavší odstín."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr "Hledaný výraz"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr "Všechny"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr "Žádný"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr "Pouze vybrané"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Interně používat jiný název"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr "Kliknutím zavřete"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Máte neuložené změny!"
|
||||
|
||||
@@ -740,20 +740,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:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
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:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
msgid "required"
|
||||
msgstr "povinný"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr "Časové pásmo:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr "Místní čas:"
|
||||
|
||||
|
||||
+2134
-1843
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: 2024-11-08 13:45+0000\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+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"
|
||||
@@ -623,40 +623,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -706,20 +706,20 @@ msgstr ""
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
msgid "required"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+2202
-1849
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: 2024-11-08 13:45+0000\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+0000\n"
|
||||
"PO-Revision-Date: 2024-07-10 15:00+0000\n"
|
||||
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -670,40 +670,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr "Ingen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr "Klik for at lukke"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Du har ændringer, der ikke er gemt!"
|
||||
|
||||
@@ -763,22 +763,22 @@ msgstr "fra %(currency)s %(price)s"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "required"
|
||||
msgstr "Kurv udløbet"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr "Tidszone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr "Din lokaltid:"
|
||||
|
||||
|
||||
+2209
-1868
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-08 13:45+0000\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+0000\n"
|
||||
"PO-Revision-Date: 2024-09-10 07:17+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -644,40 +644,40 @@ msgstr ""
|
||||
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
|
||||
"Hintergrund. Bitte wählen Sie eine dunklere Farbe."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr "Suchbegriff"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr "Keine"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr "Nur ausgewählte"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr "Geben Sie eine Seitenzahl zwischen 1 und %(max)s ein."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr "Ungültige Seitenzahl."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Intern einen anderen Namen verwenden"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr "Klicken zum Schließen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Sie haben ungespeicherte Änderungen!"
|
||||
|
||||
@@ -732,20 +732,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:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
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:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
msgid "required"
|
||||
msgstr "verpflichtend"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr "Zeitzone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr "Deine lokale Zeit:"
|
||||
|
||||
|
||||
@@ -66,10 +66,12 @@ BSD
|
||||
bspw
|
||||
Bokmål
|
||||
Boleto
|
||||
Buchhaltungs
|
||||
Bundles
|
||||
Butterfly
|
||||
bzw
|
||||
ca
|
||||
Ceuta
|
||||
Cc
|
||||
chardet
|
||||
charge
|
||||
@@ -158,9 +160,12 @@ Inc
|
||||
inkl
|
||||
innenname
|
||||
innennamen
|
||||
innergemeinschaftliche
|
||||
Innergemeinschaftlicher
|
||||
Input
|
||||
Installations
|
||||
integrationen
|
||||
intra
|
||||
INV
|
||||
invalidieren
|
||||
invalidiert
|
||||
@@ -177,10 +182,12 @@ Kombitickets
|
||||
Kompatibilitätsmodus
|
||||
Konfigurations
|
||||
Kosovo
|
||||
land
|
||||
landesspezifische
|
||||
Lead
|
||||
Leaflet
|
||||
Linktext
|
||||
lit
|
||||
Logindaten
|
||||
Lösch
|
||||
loszulegen
|
||||
@@ -188,6 +195,7 @@ Ltd
|
||||
max
|
||||
MariaDB
|
||||
MapQuest
|
||||
Melilla
|
||||
Mercado
|
||||
Merchandise
|
||||
Meta
|
||||
@@ -269,6 +277,7 @@ rückabgewickelt
|
||||
Rundungsdifferenzen
|
||||
Sa
|
||||
Saalplan
|
||||
Sammlungsstücken
|
||||
SAQ
|
||||
SCA
|
||||
Scan
|
||||
@@ -357,6 +366,7 @@ USt
|
||||
Überzahlten
|
||||
Validierung
|
||||
Venmo
|
||||
Veranstalterdomain
|
||||
Veranstaltereinstellungen
|
||||
Veranstalterkonten
|
||||
Veranstalterkonto
|
||||
@@ -367,6 +377,7 @@ Veranstalterseite
|
||||
Veranstalterübersicht
|
||||
veranstalterweiten
|
||||
Veranstaltungs
|
||||
Veranstalterdomain
|
||||
veranstaltungsweiten
|
||||
Verfügbarkeitsberechnung
|
||||
Verfügbarkeitsstatus
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-08 13:45+0000\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+0000\n"
|
||||
"PO-Revision-Date: 2024-09-10 07:17+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
@@ -643,40 +643,40 @@ msgstr ""
|
||||
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
|
||||
"Hintergrund. Bitte wähle eine dunklere Farbe."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr "Suchbegriff"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr "Keine"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr "Nur ausgewählte"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr "Gib eine Seitenzahl zwischen 1 und %(max)s ein."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr "Ungültige Seitenzahl."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Intern einen anderen Namen verwenden"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr "Klicken zum Schließen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Du hast ungespeicherte Änderungen!"
|
||||
|
||||
@@ -731,20 +731,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:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
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:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
msgid "required"
|
||||
msgstr "verpflichtend"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr "Zeitzone:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr "Deine lokale Zeit:"
|
||||
|
||||
|
||||
@@ -66,10 +66,12 @@ BSD
|
||||
bspw
|
||||
Bokmål
|
||||
Boleto
|
||||
Buchhaltungs
|
||||
Bundles
|
||||
Butterfly
|
||||
bzw
|
||||
ca
|
||||
Ceuta
|
||||
Cc
|
||||
chardet
|
||||
charge
|
||||
@@ -158,9 +160,12 @@ Inc
|
||||
inkl
|
||||
innenname
|
||||
innennamen
|
||||
innergemeinschaftliche
|
||||
Innergemeinschaftlicher
|
||||
Input
|
||||
Installations
|
||||
integrationen
|
||||
intra
|
||||
INV
|
||||
invalidieren
|
||||
invalidiert
|
||||
@@ -177,10 +182,12 @@ Kombitickets
|
||||
Kompatibilitätsmodus
|
||||
Konfigurations
|
||||
Kosovo
|
||||
land
|
||||
landesspezifische
|
||||
Lead
|
||||
Leaflet
|
||||
Linktext
|
||||
lit
|
||||
Logindaten
|
||||
Lösch
|
||||
loszulegen
|
||||
@@ -188,6 +195,7 @@ Ltd
|
||||
max
|
||||
MariaDB
|
||||
MapQuest
|
||||
Melilla
|
||||
Mercado
|
||||
Merchandise
|
||||
Meta
|
||||
@@ -269,6 +277,7 @@ rückabgewickelt
|
||||
Rundungsdifferenzen
|
||||
Sa
|
||||
Saalplan
|
||||
Sammlungsstücken
|
||||
SAQ
|
||||
SCA
|
||||
Scan
|
||||
@@ -357,6 +366,7 @@ USt
|
||||
Überzahlten
|
||||
Validierung
|
||||
Venmo
|
||||
Veranstalterdomain
|
||||
Veranstaltereinstellungen
|
||||
Veranstalterkonten
|
||||
Veranstalterkonto
|
||||
@@ -367,6 +377,7 @@ Veranstalterseite
|
||||
Veranstalterübersicht
|
||||
veranstalterweiten
|
||||
Veranstaltungs
|
||||
Veranstalterdomain
|
||||
veranstaltungsweiten
|
||||
Verfügbarkeitsberechnung
|
||||
Verfügbarkeitsstatus
|
||||
|
||||
+2119
-1846
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: 2024-11-19 15:34+0000\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+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"
|
||||
@@ -622,40 +622,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -705,20 +705,20 @@ msgstr ""
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
msgid "required"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+2251
-1878
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,9 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-08 13:45+0000\n"
|
||||
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
|
||||
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+0000\n"
|
||||
"PO-Revision-Date: 2024-12-22 00:00+0000\n"
|
||||
"Last-Translator: Dimitris Tsimpidis <tsimpidisd@gmail.com>\n"
|
||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"el/>\n"
|
||||
"Language: el\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 3.5.1\n"
|
||||
"X-Generator: Weblate 5.9.2\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -31,7 +31,7 @@ msgstr "Σχόλιο:"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34
|
||||
msgid "PayPal"
|
||||
msgstr ""
|
||||
msgstr "PayPal"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35
|
||||
msgid "Venmo"
|
||||
@@ -64,7 +64,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
|
||||
msgid "SEPA Direct Debit"
|
||||
msgstr ""
|
||||
msgstr "Τραπεζική μεταφορά"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:43
|
||||
msgid "Bancontact"
|
||||
@@ -79,10 +79,8 @@ msgid "SOFORT"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:46
|
||||
#, fuzzy
|
||||
#| msgid "Yes"
|
||||
msgid "eps"
|
||||
msgstr "Ναι"
|
||||
msgstr "EPS"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:47
|
||||
msgid "MyBank"
|
||||
@@ -147,11 +145,11 @@ msgstr "Συνέχεια"
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
||||
msgid "Confirming your payment …"
|
||||
msgstr ""
|
||||
msgstr "Επιβεβαίωση πληρωμής…"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:254
|
||||
msgid "Payment method unavailable"
|
||||
msgstr ""
|
||||
msgstr "Μη διαθέσιμος τρόπος πληρωμής"
|
||||
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
|
||||
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
|
||||
@@ -176,10 +174,8 @@ msgid "Total"
|
||||
msgstr "Σύνολο"
|
||||
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:291
|
||||
#, fuzzy
|
||||
#| msgid "Contacting Stripe …"
|
||||
msgid "Contacting your bank …"
|
||||
msgstr "Επικοινωνία με το Stripe …"
|
||||
msgstr "Επικοινωνία με την τράπεζα …"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:30
|
||||
msgid "Select a check-in list"
|
||||
@@ -686,40 +682,40 @@ msgstr ""
|
||||
"Το χρώμα σας έχει κακή αντίθεση για κείμενο σε λευκό φόντο, επιλέξτε μια πιο "
|
||||
"σκούρα σκιά."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr "Όλα"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr "Κανένας"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr "Κάντε κλικ για να κλείσετε"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -781,22 +777,22 @@ msgstr "απο %(currency)s %(price)s"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr "Εισαγάγετε μια ποσότητα για έναν από τους τύπους εισιτηρίων."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
#, fuzzy
|
||||
#| msgid "Cart expired"
|
||||
msgid "required"
|
||||
msgstr "Το καλάθι έληξε"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+2119
-1846
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: 2024-11-08 13:45+0000\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+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"
|
||||
@@ -622,40 +622,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -705,20 +705,20 @@ msgstr ""
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
msgid "required"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+2251
-1908
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,11 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-08 13:45+0000\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+0000\n"
|
||||
"PO-Revision-Date: 2024-11-18 15:48+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix-js/es/>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/es/>\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -642,40 +642,40 @@ msgstr ""
|
||||
"Tu color tiene mal contraste para un texto con fondo blanco, por favor, "
|
||||
"escoge un tono más oscuro."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr "Consulta de búsqueda"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr "Todos"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr "Ninguno"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr "Solamente seleccionados"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr "Introduce un número de página entre 1 y %(max)s."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr "Número de página inválido."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Usar un nombre diferente internamente"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr "Click para cerrar"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "¡Tienes cambios sin guardar!"
|
||||
|
||||
@@ -730,20 +730,20 @@ msgstr "Obtienes %(currency)s %(price)s de vuelta"
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr "Por favor, ingrese el importe que el organizador puede quedarse."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
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:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
msgid "required"
|
||||
msgstr "campo requerido"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr "Zona horaria:"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr "Su hora local:"
|
||||
|
||||
|
||||
+2119
-1846
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: 2024-11-08 13:45+0000\n"
|
||||
"POT-Creation-Date: 2024-12-16 14:20+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"
|
||||
@@ -622,40 +622,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:496
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:516
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:442
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:460
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:519
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:465
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:862
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:808
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:865
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:811
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1023
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:969
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1063
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1009
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1138
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1084
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
@@ -705,20 +705,20 @@ msgstr ""
|
||||
msgid "Please enter the amount the organizer can keep."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:449
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:454
|
||||
msgid "Please enter a quantity for one of the ticket types."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:485
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:506
|
||||
msgid "required"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:588
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:607
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:550
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:569
|
||||
msgid "Time zone:"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:598
|
||||
#: pretix/static/pretixpresale/js/ui/main.js:560
|
||||
msgid "Your local time:"
|
||||
msgstr ""
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user