forked from CGM_Public/pretix_original
Compare commits
217 Commits
loadtest
...
794-questi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df25ae3c7c | ||
|
|
d686b386e3 | ||
|
|
bdda68c82d | ||
|
|
2aa36ee2a7 | ||
|
|
43affc22ab | ||
|
|
60575377d7 | ||
|
|
5ec8c7ed96 | ||
|
|
ef55a018f8 | ||
|
|
6bf9327f87 | ||
|
|
f761f93550 | ||
|
|
c996289563 | ||
|
|
89bbed42e6 | ||
|
|
8e22c0f3a4 | ||
|
|
3483c522da | ||
|
|
5f15ebc46f | ||
|
|
3415fd947a | ||
|
|
a70a42c273 | ||
|
|
697cdfd5c9 | ||
|
|
d8a7de8b23 | ||
|
|
9f7f0e74ff | ||
|
|
7ef289da45 | ||
|
|
e82bc732a3 | ||
|
|
4636ccac3b | ||
|
|
e3518bfb4b | ||
|
|
b2471169af | ||
|
|
487418678c | ||
|
|
d4795868d6 | ||
|
|
45af18a23d | ||
|
|
a6de586b80 | ||
|
|
e6859fa82b | ||
|
|
2d5e14e517 | ||
|
|
7219575b84 | ||
|
|
991e4127f6 | ||
|
|
420649e10a | ||
|
|
0d02e2fe8c | ||
|
|
afdba9f268 | ||
|
|
394f7e04c3 | ||
|
|
c3a5cef051 | ||
|
|
47b7bcbfca | ||
|
|
2cd1345035 | ||
|
|
c24ce551ba | ||
|
|
0bb6e460e8 | ||
|
|
26257f0829 | ||
|
|
6badb342bf | ||
|
|
865a70d5d5 | ||
|
|
df1c0d4f3a | ||
|
|
adb982c451 | ||
|
|
94ba26d841 | ||
|
|
45d5487eb5 | ||
|
|
38f5f75a1b | ||
|
|
90f881c48e | ||
|
|
c4b18a4c81 | ||
|
|
8e2ef604f7 | ||
|
|
970e4f6d52 | ||
|
|
59c9731bae | ||
|
|
8afd09a647 | ||
|
|
fa375950a7 | ||
|
|
ccdfa716c0 | ||
|
|
08ffa17e01 | ||
|
|
7f8c91ec9d | ||
|
|
dde3a53e09 | ||
|
|
73bc3259e8 | ||
|
|
34767a2029 | ||
|
|
85bd1a0d44 | ||
|
|
1ad4b6019e | ||
|
|
a778675857 | ||
|
|
e5a980aef4 | ||
|
|
e8338a2941 | ||
|
|
289bbf84a9 | ||
|
|
f13dbb85cb | ||
|
|
49e706a580 | ||
|
|
ca7d55082b | ||
|
|
516fab52da | ||
|
|
ddf6af278c | ||
|
|
07b4b8c473 | ||
|
|
a0af0cfb06 | ||
|
|
3eb86a371a | ||
|
|
ae7175c00b | ||
|
|
4286176e73 | ||
|
|
0f53ab67df | ||
|
|
0d6db082ca | ||
|
|
2a9b8baa98 | ||
|
|
6bdaab5319 | ||
|
|
489d5e3d01 | ||
|
|
86e2cf2786 | ||
|
|
f075dbc78e | ||
|
|
10500ee6a9 | ||
|
|
6c766d872d | ||
|
|
c8b1206e61 | ||
|
|
424d1489bf | ||
|
|
b3e567a188 | ||
|
|
66e42f66e5 | ||
|
|
786fbc6e29 | ||
|
|
f2ff5d7510 | ||
|
|
0bcc4de2de | ||
|
|
586e7cc997 | ||
|
|
bf33cc1499 | ||
|
|
faff7b4166 | ||
|
|
44263a17e6 | ||
|
|
87b4d1aaed | ||
|
|
0d12c1589a | ||
|
|
96ddd8ce4e | ||
|
|
dc2da88220 | ||
|
|
b8796b0632 | ||
|
|
961b1d4efa | ||
|
|
ac674565cf | ||
|
|
301e9d1d48 | ||
|
|
81e08021db | ||
|
|
da1beac49c | ||
|
|
a81fc5bfe0 | ||
|
|
eb3ec0f99a | ||
|
|
7e98846315 | ||
|
|
22f9178617 | ||
|
|
ef73abc3f6 | ||
|
|
ac6e6a526e | ||
|
|
8185c6a0d6 | ||
|
|
a7b294fc61 | ||
|
|
b81f07b237 | ||
|
|
67bdb0ec1f | ||
|
|
7b6ff01740 | ||
|
|
83f5182db2 | ||
|
|
ee2050b8f9 | ||
|
|
185fc6c73d | ||
|
|
a21ea34944 | ||
|
|
130ba3c217 | ||
|
|
dd39131942 | ||
|
|
295ad9e9c3 | ||
|
|
a0969dc7fa | ||
|
|
81a2e0c71c | ||
|
|
71f69c9afb | ||
|
|
cfff6f1605 | ||
|
|
84ccaed94a | ||
|
|
ec61e07ab6 | ||
|
|
4d5fb67b02 | ||
|
|
c885807fa5 | ||
|
|
88ec54809a | ||
|
|
5f96152d57 | ||
|
|
a9b08660c6 | ||
|
|
68e901f76d | ||
|
|
e10083379c | ||
|
|
d705102cbb | ||
|
|
122fda27c4 | ||
|
|
b83752005a | ||
|
|
7fc926f23e | ||
|
|
d90686f352 | ||
|
|
8523f4dfa2 | ||
|
|
9c13676349 | ||
|
|
19ee8e9802 | ||
|
|
bbe5ff249b | ||
|
|
7470cda17f | ||
|
|
a4d50ae4c5 | ||
|
|
e0d7a9d2da | ||
|
|
2159a65643 | ||
|
|
3f05c92602 | ||
|
|
d764cdb338 | ||
|
|
7b7bd67ae9 | ||
|
|
dc502618dd | ||
|
|
cb6b4c96f8 | ||
|
|
de5f094f73 | ||
|
|
6b85e89e62 | ||
|
|
8cfc8bc152 | ||
|
|
27990b3fbb | ||
|
|
307ee36e52 | ||
|
|
f95e8f374d | ||
|
|
d10cbd07a7 | ||
|
|
5519643782 | ||
|
|
2c91a17927 | ||
|
|
875d79536b | ||
|
|
4bf0d2d229 | ||
|
|
068983004a | ||
|
|
0365a1c68d | ||
|
|
5024fae5ed | ||
|
|
affc6254a8 | ||
|
|
bb956c13ba | ||
|
|
ee70104735 | ||
|
|
8ba38a0254 | ||
|
|
761a03abdc | ||
|
|
f3b63acd40 | ||
|
|
9eee967050 | ||
|
|
02aee0637a | ||
|
|
dde99c45f3 | ||
|
|
74292535ad | ||
|
|
d1e3ba778d | ||
|
|
475835959d | ||
|
|
efdeaeac83 | ||
|
|
56fe37dd67 | ||
|
|
ca07f48afd | ||
|
|
fa706549ce | ||
|
|
989b28c2f6 | ||
|
|
1c84660c42 | ||
|
|
cf58447cd4 | ||
|
|
305a3aaf9f | ||
|
|
b54a8c120f | ||
|
|
89684c8e0f | ||
|
|
c5566dfee7 | ||
|
|
5e7ee3c047 | ||
|
|
815ee29a50 | ||
|
|
13ee691133 | ||
|
|
4e3dd24209 | ||
|
|
7ef4adeb73 | ||
|
|
7be5331da5 | ||
|
|
12fc02b2e4 | ||
|
|
86b4835273 | ||
|
|
e53818b025 | ||
|
|
206a0a28c7 | ||
|
|
461b0b639c | ||
|
|
2e6f5d0f32 | ||
|
|
12b48948e3 | ||
|
|
87c7a3d26f | ||
|
|
4c0789ac20 | ||
|
|
bc4e6fa549 | ||
|
|
2b8949dea4 | ||
|
|
f3ef00e3b7 | ||
|
|
c5499df0b4 | ||
|
|
68dbfedfdf | ||
|
|
e70738ae0c | ||
|
|
5750201bc3 |
@@ -58,16 +58,29 @@ Database
|
||||
--------
|
||||
|
||||
Next, we need a database and a database user. We can create these with any kind of database managing tool or directly on
|
||||
our database's shell, e.g. for MySQL::
|
||||
our database's shell. For PostgreSQL, we would do::
|
||||
|
||||
$ mysql -u root -p
|
||||
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
|
||||
mysql> FLUSH PRIVILEGES;
|
||||
# sudo -u postgres createuser -P pretix
|
||||
# sudo -u postgres createdb -O pretix pretix
|
||||
|
||||
Replace the asterisks with a password of your own. For MySQL, we will use a unix domain socket to connect to the
|
||||
database. For PostgreSQL, be sure to configure the interface binding and your firewall so that the docker container
|
||||
can reach PostgreSQL.
|
||||
Make sure that your database listens on the network. If PostgreSQL on the same same host as docker, but not inside a docker container, we recommend that you just listen on the Docker interface by changing the following line in ``/etc/postgresql/<version>/main/postgresql.conf``::
|
||||
|
||||
listen_addresses = 'localhost,172.17.0.1'
|
||||
|
||||
You also need to add a new line to ``/etc/postgresql/<version>/main/pg_hba.conf`` to allow network connections to this user and database::
|
||||
|
||||
host pretix pretix 172.17.0.1/16 md5
|
||||
|
||||
Restart PostgreSQL after you changed these files::
|
||||
|
||||
# systemctl restart postgresql
|
||||
|
||||
If you have a firewall running, you should also make sure that port 5432 is reachable from the ``172.17.0.1/16`` subnet.
|
||||
|
||||
For MySQL, you can either also use network-based connections or mount the ``/var/run/mysqld/mysqld.sock`` socket into the docker container.
|
||||
When using MySQL, make sure you set the character set of the database to ``utf8mb4``, e.g. like this::
|
||||
|
||||
mysql > CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
Redis
|
||||
-----
|
||||
@@ -114,13 +127,16 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
datadir=/data
|
||||
|
||||
[database]
|
||||
; Replace mysql with postgresql_psycopg2 for PostgreSQL
|
||||
backend=mysql
|
||||
; Replace postgresql with mysql for MySQL
|
||||
backend=postgresql
|
||||
name=pretix
|
||||
user=pretix
|
||||
; Replace with the password you chose above
|
||||
password=*********
|
||||
; Replace with host IP address for PostgreSQL
|
||||
host=/var/run/mysqld/mysqld.sock
|
||||
; In most docker setups, 172.17.0.1 is the address of the docker host. Adjuts
|
||||
; this to wherever your database is running, e.g. the name of a linked container
|
||||
; or of a mounted MySQL socket.
|
||||
host=172.17.0.1
|
||||
|
||||
[mail]
|
||||
; See config file documentation for more options
|
||||
@@ -164,14 +180,15 @@ named ``/etc/systemd/system/pretix.service`` with the following content::
|
||||
-v /var/pretix-data:/data \
|
||||
-v /etc/pretix:/etc/pretix \
|
||||
-v /var/run/redis:/var/run/redis \
|
||||
-v /var/run/mysqld:/var/run/mysqld \
|
||||
pretix/standalone:stable all
|
||||
ExecStop=/usr/bin/docker stop %n
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
You can leave the MySQL socket volume out if you're using PostgreSQL. You can now run the following commands
|
||||
When using MySQL and socket mounting, you'll need the additional flag ``-v /var/run/mysqld:/var/run/mysqld`` in the command.
|
||||
|
||||
You can now run the following commands
|
||||
to enable and start the service::
|
||||
|
||||
# systemctl daemon-reload
|
||||
|
||||
@@ -50,21 +50,27 @@ Database
|
||||
--------
|
||||
|
||||
Having the database server installed, we still need a database and a database user. We can create these with any kind
|
||||
of database managing tool or directly on our database's shell, e.g. for MySQL::
|
||||
of database managing tool or directly on our database's shell. For PostgreSQL, we would do::
|
||||
|
||||
$ mysql -u root -p
|
||||
mysql> CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
mysql> GRANT ALL PRIVILEGES ON pretix.* TO pretix@'localhost' IDENTIFIED BY '*********';
|
||||
mysql> FLUSH PRIVILEGES;
|
||||
# sudo -u postgres createuser pretix
|
||||
# sudo -u postgres createdb -O pretix pretix
|
||||
|
||||
When using MySQL, make sure you set the character set of the database to ``utf8mb4``, e.g. like this::
|
||||
|
||||
mysql > CREATE DATABASE pretix DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
Package dependencies
|
||||
--------------------
|
||||
|
||||
To build and run pretix, you will need the following debian packages::
|
||||
|
||||
# apt-get install git build-essential python-dev python-virtualenv python3 python3-pip \
|
||||
# apt-get install git build-essential python-dev python3-venv python3 python3-pip \
|
||||
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
||||
gettext libpq-dev libmysqlclient-dev libjpeg-dev libopenjp2-7-dev
|
||||
gettext libpq-dev libmariadbclient-dev libjpeg-dev libopenjp2-7-dev
|
||||
|
||||
.. note:: Python 3.7 is not yet supported, so if you run a very recent OS, make sure to get
|
||||
Python 3.6 from somewhere. You can check the current state of things in our
|
||||
`Python 3.7 issue`_.
|
||||
|
||||
Config file
|
||||
-----------
|
||||
@@ -85,13 +91,18 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
datadir=/var/pretix/data
|
||||
|
||||
[database]
|
||||
; Replace mysql with postgresql_psycopg2 for PostgreSQL
|
||||
backend=mysql
|
||||
; For MySQL, replace with "mysql"
|
||||
backend=postgresql
|
||||
name=pretix
|
||||
user=pretix
|
||||
password=*********
|
||||
; Replace with host IP address for PostgreSQL
|
||||
host=/var/run/mysqld/mysqld.sock
|
||||
; For MySQL, enter the user password. For PostgreSQL on the same host,
|
||||
; we don't need one because we can use peer authentification if our
|
||||
; PostgreSQL user matches our unix user.
|
||||
password=
|
||||
; For MySQL, use local socket, e.g. /var/run/mysqld/mysqld.sock
|
||||
; For a remote host, supply an IP address
|
||||
; For local postgres authentication, you can leave it empty
|
||||
host=
|
||||
|
||||
[mail]
|
||||
; See config file documentation for more options
|
||||
@@ -115,14 +126,14 @@ Now we will install pretix itself. The following steps are to be executed as the
|
||||
actually install pretix, we will create a virtual environment to isolate the python packages from your global
|
||||
python installation::
|
||||
|
||||
$ virtualenv -p python3 /var/pretix/venv
|
||||
$ python3 -m venv /var/pretix/venv
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U pip setuptools wheel
|
||||
|
||||
We now install pretix, its direct dependencies and gunicorn. Replace ``mysql`` with ``postgres`` in the following
|
||||
command if you're running PostgreSQL::
|
||||
We now install pretix, its direct dependencies and gunicorn. Replace ``postgres`` with ``mysql`` in the following
|
||||
command if you're running MySQL::
|
||||
|
||||
(venv)$ pip3 install "pretix[mysql]" gunicorn
|
||||
(venv)$ pip3 install "pretix[postgres]" gunicorn
|
||||
|
||||
Note that you need Python 3.5 or newer. You can find out your Python version using ``python -V``.
|
||||
|
||||
@@ -268,10 +279,10 @@ Updates
|
||||
.. warning:: While we try hard not to break things, **please perform a backup before every upgrade**.
|
||||
|
||||
To upgrade to a new pretix release, pull the latest code changes and run the following commands (again, replace
|
||||
``mysql`` with ``postgres`` if necessary)::
|
||||
``postgres`` with ``mysql`` if necessary)::
|
||||
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U pretix[mysql] gunicorn
|
||||
(venv)$ pip3 install -U pretix[postgres] gunicorn
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updatestyles
|
||||
@@ -303,3 +314,4 @@ example::
|
||||
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
|
||||
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
|
||||
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||
.. _Python 3.7 issue: https://github.com/pretix/pretix/issues/1025
|
||||
|
||||
@@ -20,7 +20,7 @@ internal_name string An optional nam
|
||||
description multi-lingual string A public description (might include markdown, can
|
||||
be ``null``)
|
||||
position integer An integer, used for sorting the categories
|
||||
is_addon boolean If ``True``, items within this category are not on sale
|
||||
is_addon boolean If ``true``, items within this category are not on sale
|
||||
on their own but the category provides a source for
|
||||
defining add-ons for other products.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
@@ -156,14 +156,14 @@ Endpoints
|
||||
"checkin_count": 17,
|
||||
"position_count": 42,
|
||||
"event": {
|
||||
"name": "Demo Converence",
|
||||
"name": "Demo Conference"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"name": "T-Shirt",
|
||||
"id": 1,
|
||||
"checkin_count": 1,
|
||||
"admission": False,
|
||||
"admission": false,
|
||||
"position_count": 1,
|
||||
"variations": [
|
||||
{
|
||||
@@ -184,7 +184,7 @@ Endpoints
|
||||
"name": "Ticket",
|
||||
"id": 2,
|
||||
"checkin_count": 15,
|
||||
"admission": True,
|
||||
"admission": true,
|
||||
"position_count": 22,
|
||||
"variations": []
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ is_public boolean If ``true``, th
|
||||
presale_start datetime The date at which the ticket shop opens (or ``null``)
|
||||
presale_end datetime The date at which the ticket shop closes (or ``null``)
|
||||
location multi-lingual string The event location (or ``null``)
|
||||
has_subevents boolean ``True`` if the event series feature is active for this
|
||||
has_subevents boolean ``true`` if the event series feature is active for this
|
||||
event. Cannot change after event is created.
|
||||
meta_data dict Values set for organizer-specific meta data parameters.
|
||||
plugins list A list of package names of the enabled plugins for this
|
||||
@@ -379,7 +379,7 @@ Endpoints
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to update
|
||||
:param event: The ``slug`` field of the event to update
|
||||
:statuscode 201: no error
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The event could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||
|
||||
@@ -11,6 +11,7 @@ Resources and endpoints
|
||||
categories
|
||||
items
|
||||
item_variations
|
||||
item_bundles
|
||||
item_add-ons
|
||||
questions
|
||||
question_options
|
||||
|
||||
@@ -13,7 +13,7 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
number string Invoice number (with prefix)
|
||||
order string Order code of the order this invoice belongs to
|
||||
is_cancellation boolean ``True``, if this invoice is the cancellation of a
|
||||
is_cancellation boolean ``true``, if this invoice is the cancellation of a
|
||||
different invoice.
|
||||
invoice_from string Sender address
|
||||
invoice_to string Receiver address
|
||||
|
||||
@@ -189,7 +189,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"min_count": 0,
|
||||
"max_count": 10,
|
||||
"max_count": 10
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
242
doc/api/resources/item_bundles.rst
Normal file
242
doc/api/resources/item_bundles.rst
Normal file
@@ -0,0 +1,242 @@
|
||||
Item bundles
|
||||
============
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
With bundles, you can specify products that are included within other products. There are two premier use cases of this:
|
||||
|
||||
* Package discounts. For example, you could offer a discounted package that includes three tickets but can only be
|
||||
bought as a whole. With a bundle including three times the usual product, the package will automatically pull three
|
||||
sub-items into the cart, making sure of correct quota calculation and issuance of the correct number of tickets.
|
||||
|
||||
* Tax splitting. For example, if your conference ticket includes a part that is subject to different taxation and that
|
||||
you need to put on the invoice separately. When you putting a "designated price" on a bundled sub-item, pretix will
|
||||
use that price to show a split taxation.
|
||||
|
||||
The bundles resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the bundling configuration
|
||||
bundled_item integer Internal ID of the item that is included.
|
||||
bundled_variation integer Internal ID of the variation of the item (or ``null``).
|
||||
count integer Number of items included
|
||||
designated_price money (string) Designated price of the bundled product. This will be
|
||||
used to split the price of the base item e.g. for mixed
|
||||
taxation. This is not added to the price.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
|
||||
This resource has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/bundles/
|
||||
|
||||
Returns a list of all bundles for a given item.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/items/11/bundles/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 2,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 3,
|
||||
"bundled_item": 3,
|
||||
"bundled_variation": null,
|
||||
"count": 1,
|
||||
"designated_price": "0.00"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"bundled_item": 3,
|
||||
"bundled_variation": null,
|
||||
"count": 2,
|
||||
"designated_price": "1.50"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param item: The ``id`` field of the item to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event/item does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/bundles/(id)/
|
||||
|
||||
Returns information on one bundle configuration, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/items/1/bundles/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 3,
|
||||
"bundled_item": 3,
|
||||
"bundled_variation": null,
|
||||
"count": 2,
|
||||
"designated_price": "1.50"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param item: The ``id`` field of the item to fetch
|
||||
:param id: The ``id`` field of the bundle to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/bigevents/events/sampleconf/items/1/bundles/
|
||||
|
||||
Creates a new bundle configuration
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/(organizer)/events/(event)/items/(item)/bundles/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
|
||||
{
|
||||
"bundled_item": 3,
|
||||
"bundled_variation": null,
|
||||
"count": 2,
|
||||
"designated_price": "1.50"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 3,
|
||||
"bundled_item": 3,
|
||||
"bundled_variation": null,
|
||||
"count": 2,
|
||||
"designated_price": "1.50"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a bundle-configuration for
|
||||
:param event: The ``slug`` field of the event to create a bundle configuration for
|
||||
:param item: The ``id`` field of the item to create a bundle configuration for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The bundle could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/bundles/(id)/
|
||||
|
||||
Update a bundle configuration. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all
|
||||
fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields
|
||||
that you want to change.
|
||||
|
||||
You can change all fields of the resource except the ``id`` field.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/items/1/bundles/3/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"count": 2
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 3,
|
||||
"bundled_item": 3,
|
||||
"bundled_variation": null,
|
||||
"count": 2,
|
||||
"designated_price": "1.50"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param item: The ``id`` field of the item to modify
|
||||
:param id: The ``id`` field of the bundle to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The bundle configuration could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/items/(id)/bundles/(id)/
|
||||
|
||||
Delete a bundle configuration.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/bundles/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param id: The ``id`` field of the item to modify
|
||||
:param id: The ``id`` field of the bundle to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||
@@ -18,7 +18,7 @@ default_price money (string) The price set d
|
||||
price money (string) The price used for this variation. This is either the
|
||||
same as ``default_price`` if that value is set or equal
|
||||
to the item's ``default_price`` (read-only).
|
||||
active boolean If ``False``, this variation will not be sold or shown.
|
||||
active boolean If ``false``, this variation will not be sold or shown.
|
||||
description multi-lingual string A public description of the variation. May contain
|
||||
Markdown syntax or can be ``null``.
|
||||
position integer An integer, used for sorting
|
||||
|
||||
@@ -21,34 +21,34 @@ default_price money (string) The item price
|
||||
overwritten by variations or other options.
|
||||
category integer The ID of the category this item belongs to
|
||||
(or ``null``).
|
||||
active boolean If ``False``, the item is hidden from all public lists
|
||||
active boolean If ``false``, the item is hidden from all public lists
|
||||
and will not be sold.
|
||||
description multi-lingual string A public description of the item. May contain Markdown
|
||||
syntax or can be ``null``.
|
||||
free_price boolean If ``True``, customers can change the price at which
|
||||
free_price boolean If ``true``, customers can change the price at which
|
||||
they buy the product (however, the price can't be set
|
||||
lower than the price defined by ``default_price`` or
|
||||
otherwise).
|
||||
tax_rate decimal (string) The VAT rate to be applied for this item.
|
||||
tax_rule integer The internal ID of the applied tax rule (or ``null``).
|
||||
admission boolean ``True`` for items that grant admission to the event
|
||||
(such as primary tickets) and ``False`` for others
|
||||
admission boolean ``true`` for items that grant admission to the event
|
||||
(such as primary tickets) and ``false`` for others
|
||||
(such as add-ons or merchandise).
|
||||
position integer An integer, used for sorting
|
||||
picture string A product picture to be displayed in the shop
|
||||
(read-only).
|
||||
(read-only, can be ``null``).
|
||||
sales_channels list of strings Sales channels this product is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
available_from datetime The first date time at which this item can be bought
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this item can be bought
|
||||
(or ``null``).
|
||||
require_voucher boolean If ``True``, this item can only be bought using a
|
||||
require_voucher boolean If ``true``, this item can only be bought using a
|
||||
voucher that is specifically assigned to this item.
|
||||
hide_without_voucher boolean If ``True``, this item is only shown during the voucher
|
||||
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
|
||||
redemption process, but not in the normal shop
|
||||
frontend.
|
||||
allow_cancel boolean If ``False``, customers cannot cancel orders containing
|
||||
allow_cancel boolean If ``false``, customers cannot cancel orders containing
|
||||
this item.
|
||||
min_per_order integer This product can only be bought if it is included at
|
||||
least this many times in the order (or ``null`` for no
|
||||
@@ -56,16 +56,17 @@ min_per_order integer This product ca
|
||||
max_per_order integer This product can only be bought if it is included at
|
||||
most this many times in the order (or ``null`` for no
|
||||
limitation).
|
||||
checkin_attention boolean If ``True``, the check-in app should show a warning
|
||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if such
|
||||
a product is being scanned.
|
||||
original_price money (string) An original price, shown for comparison, not used
|
||||
for price calculations.
|
||||
require_approval boolean If ``True``, orders with this product will need to be
|
||||
for price calculations (or ``null``).
|
||||
require_approval boolean If ``true``, orders with this product will need to be
|
||||
approved by the event organizer before they can be
|
||||
paid.
|
||||
generate_tickets boolean If ``False``, tickets are never generated for this
|
||||
product, regardless of other settings. If ``True``,
|
||||
require_bundling boolean If ``true``, this item is only available as part of bundles.
|
||||
generate_tickets boolean If ``false``, tickets are never generated for this
|
||||
product, regardless of other settings. If ``true``,
|
||||
tickets are generated even if this is a
|
||||
non-admission or add-on product, regardless of event
|
||||
settings. If this is ``null``, regular ticketing
|
||||
@@ -80,7 +81,7 @@ variations list of objects A list with one
|
||||
├ price money (string) The price used for this variation. This is either the
|
||||
same as ``default_price`` if that value is set or equal
|
||||
to the item's ``default_price``.
|
||||
├ active boolean If ``False``, this variation will not be sold or shown.
|
||||
├ active boolean If ``false``, this variation will not be sold or shown.
|
||||
├ description multi-lingual string A public description of the variation. May contain
|
||||
Markdown syntax or can be ``null``.
|
||||
└ position integer An integer, used for sorting
|
||||
@@ -91,8 +92,17 @@ addons list of objects Definition of a
|
||||
chosen from.
|
||||
├ min_count integer The minimal number of add-ons that need to be chosen.
|
||||
├ max_count integer The maximal number of add-ons that can be chosen.
|
||||
└ position integer An integer, used for sorting
|
||||
├ position integer An integer, used for sorting
|
||||
└ price_included boolean Adding this add-on to the item is free
|
||||
bundles list of objects Definition of bundles that are included in this item.
|
||||
Only writable during creation,
|
||||
use separate endpoint to modify this later.
|
||||
├ bundled_item integer Internal ID of the item that is included.
|
||||
├ bundled_variation integer Internal ID of the variation of the item (or ``null``).
|
||||
├ count integer Number of items included
|
||||
└ designated_price money (string) Designated price of the bundled product. This will be
|
||||
used to split the price of the base item e.g. for mixed
|
||||
taxation. This is not added to the price.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
@@ -121,15 +131,20 @@ addons list of objects Definition of a
|
||||
|
||||
The ``generate_tickets`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
|
||||
The ``bundles`` and ``require_bundling`` attributes have been added.
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
Please note that an item either always has variations or never has. Once created with variations the item can never
|
||||
change to an item without and vice versa. To create an item with variations ensure that you POST an item with at least
|
||||
one variation.
|
||||
|
||||
Also note that ``variations`` and ``addons`` are only supported on ``POST``. To update/delete variations and add-ons please
|
||||
use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT`` with nested
|
||||
``variations`` and/or ``addons``.
|
||||
Also note that ``variations``, ``bundles``, and ``addons`` are only supported on ``POST``. To update/delete variations,
|
||||
bundles, and add-ons please use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT``
|
||||
with nested ``variations``, ``bundles`` and/or ``addons``.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -186,6 +201,7 @@ Endpoints
|
||||
"has_variations": false,
|
||||
"generate_tickets": null,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
"variations": [
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
@@ -204,7 +220,8 @@ Endpoints
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"addons": []
|
||||
"addons": [],
|
||||
"bundles": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -273,6 +290,7 @@ Endpoints
|
||||
"checkin_attention": false,
|
||||
"has_variations": false,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
"variations": [
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
@@ -291,7 +309,8 @@ Endpoints
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"addons": []
|
||||
"addons": [],
|
||||
"bundles": []
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -340,6 +359,7 @@ Endpoints
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
"variations": [
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
@@ -358,7 +378,8 @@ Endpoints
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"addons": []
|
||||
"addons": [],
|
||||
"bundles": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -396,6 +417,7 @@ Endpoints
|
||||
"checkin_attention": false,
|
||||
"has_variations": true,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
"variations": [
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
@@ -414,7 +436,8 @@ Endpoints
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"addons": []
|
||||
"addons": [],
|
||||
"bundles": []
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to create an item for
|
||||
@@ -483,6 +506,7 @@ Endpoints
|
||||
"checkin_attention": false,
|
||||
"has_variations": true,
|
||||
"require_approval": false,
|
||||
"require_bundling": false,
|
||||
"variations": [
|
||||
{
|
||||
"value": {"en": "Student"},
|
||||
@@ -501,7 +525,8 @@ Endpoints
|
||||
"position": 1
|
||||
}
|
||||
],
|
||||
"addons": []
|
||||
"addons": [],
|
||||
"bundles": []
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
|
||||
@@ -26,7 +26,7 @@ status string Order status, o
|
||||
* ``p`` – paid
|
||||
* ``e`` – expired
|
||||
* ``c`` – canceled
|
||||
testmode boolean If ``True``, this order was created when the event was in
|
||||
testmode boolean If ``true``, this order was created when the event was in
|
||||
test mode. Only orders in test mode can be deleted.
|
||||
secret string The secret contained in the link sent to the customer
|
||||
email string The customer email address
|
||||
@@ -39,13 +39,13 @@ payment_date date **DEPRECATED AN
|
||||
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
||||
total money (string) Total value of this order
|
||||
comment string Internal comment on this order
|
||||
checkin_attention boolean If ``True``, the check-in app should show a warning
|
||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if a ticket
|
||||
of this order is scanned.
|
||||
invoice_address object Invoice address information (can be ``null``)
|
||||
├ last_modified datetime Last modification date of the address
|
||||
├ company string Customer company name
|
||||
├ is_business boolean Business or individual customers (always ``False``
|
||||
├ is_business boolean Business or individual customers (always ``false``
|
||||
for orders created before pretix 1.7, do not rely on
|
||||
it).
|
||||
├ name string Customer name
|
||||
@@ -56,7 +56,7 @@ invoice_address object Invoice address
|
||||
├ country string Customer country
|
||||
├ internal_reference string Customer's internal reference to be printed on the invoice
|
||||
├ vat_id string Customer VAT ID
|
||||
└ vat_id_validated string ``True``, if the VAT ID has been validated against the
|
||||
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
||||
EU VAT service and validation was successful. This only
|
||||
happens in rare cases.
|
||||
positions list of objects List of non-canceled order positions (see below)
|
||||
@@ -78,9 +78,9 @@ downloads list of objects List of ticket
|
||||
download options.
|
||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||
└ url string Download URL
|
||||
require_approval boolean If ``True`` and the order is pending, this order
|
||||
require_approval boolean If ``true`` and the order is pending, this order
|
||||
needs approval by an organizer before it can
|
||||
continue. If ``True`` and the order is canceled,
|
||||
continue. If ``true`` and the order is canceled,
|
||||
this order has been denied by the event organizer.
|
||||
payments list of objects List of payment processes (see below)
|
||||
refunds list of objects List of refund processes (see below)
|
||||
@@ -295,7 +295,7 @@ List of all orders
|
||||
"require_approval": false,
|
||||
"invoice_address": {
|
||||
"last_modified": "2017-12-01T10:00:00Z",
|
||||
"is_business": True,
|
||||
"is_business": true,
|
||||
"company": "Sample company",
|
||||
"name": "John Doe",
|
||||
"name_parts": {"full_name": "John Doe"},
|
||||
@@ -305,7 +305,7 @@ List of all orders
|
||||
"country": "Testikistan",
|
||||
"internal_reference": "",
|
||||
"vat_id": "EU123456789",
|
||||
"vat_id_validated": False
|
||||
"vat_id_validated": false
|
||||
},
|
||||
"positions": [
|
||||
{
|
||||
@@ -437,7 +437,7 @@ Fetching individual orders
|
||||
"invoice_address": {
|
||||
"last_modified": "2017-12-01T10:00:00Z",
|
||||
"company": "Sample company",
|
||||
"is_business": True,
|
||||
"is_business": true,
|
||||
"name": "John Doe",
|
||||
"name_parts": {"full_name": "John Doe"},
|
||||
"street": "Test street 12",
|
||||
@@ -446,7 +446,7 @@ Fetching individual orders
|
||||
"country": "Testikistan",
|
||||
"internal_reference": "",
|
||||
"vat_id": "EU123456789",
|
||||
"vat_id_validated": False
|
||||
"vat_id_validated": false
|
||||
},
|
||||
"positions": [
|
||||
{
|
||||
@@ -563,6 +563,92 @@ Order ticket download
|
||||
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
||||
seconds.
|
||||
|
||||
Updating order fields
|
||||
---------------------
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/
|
||||
|
||||
Updates specific fields on an order. Currently, only the following fields are supported:
|
||||
|
||||
* ``email``
|
||||
|
||||
* ``checkin_attention``
|
||||
|
||||
* ``locale``
|
||||
|
||||
* ``comment``
|
||||
|
||||
* ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address)
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "other@example.org",
|
||||
"locale": "de",
|
||||
"comment": "Foo",
|
||||
"checkin_attention": true
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
(Full order resource, see above.)
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event
|
||||
:param event: The ``slug`` field of the event
|
||||
:param code: The ``code`` field of the order to update
|
||||
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order could not be updated due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
|
||||
|
||||
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.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/regenerate_secrets/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
(Full order resource, see above.)
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event
|
||||
:param event: The ``slug`` field of the event
|
||||
:param code: The ``code`` field of the order to update
|
||||
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order could not be updated due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order.
|
||||
|
||||
Deleting orders
|
||||
---------------
|
||||
|
||||
@@ -714,7 +800,7 @@ Creating orders
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "dummy@example.org",
|
||||
@@ -731,7 +817,7 @@ Creating orders
|
||||
],
|
||||
"payment_provider": "banktransfer",
|
||||
"invoice_address": {
|
||||
"is_business": False,
|
||||
"is_business": false,
|
||||
"company": "Sample company",
|
||||
"name_parts": {"full_name": "John Doe"},
|
||||
"street": "Sesam Street 12",
|
||||
@@ -774,10 +860,10 @@ Creating orders
|
||||
|
||||
(Full order resource, see above.)
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to create an item for
|
||||
:param event: The ``slug`` field of the event to create an item for
|
||||
:param organizer: The ``slug`` field of the organizer of the event to create an order for
|
||||
:param event: The ``slug`` field of the event to create an order for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The item could not be created due to invalid submitted data or lack of quota.
|
||||
:statuscode 400: The order could not be created due to invalid submitted data or lack of quota.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
|
||||
order.
|
||||
@@ -902,7 +988,7 @@ Order state operations
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_expired/
|
||||
|
||||
Marks a unpaid order as expired.
|
||||
Marks an unpaid order as expired.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@@ -1061,9 +1147,82 @@ Order state operations
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order cannot be marked as denied since the current order status does not allow it.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
Generating invoices
|
||||
-------------------
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/create_invoice/
|
||||
|
||||
Creates an invoice for an order which currently does not have an invoice. Returns the
|
||||
invoice object.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/create_invoice/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"order": "FOO",
|
||||
"number": "DUMMY-00001",
|
||||
"is_cancellation": false,
|
||||
...
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param code: The ``code`` field of the order to create an invoice for
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The invoice can not be created (invoicing disabled, the order already has an invoice, …)
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
|
||||
Sending e-mails
|
||||
---------------
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/resend_link/
|
||||
|
||||
Sends an email to the buyer with the link to the order page.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/resend_link/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param event: The ``slug`` field of the event to modify
|
||||
:param code: The ``code`` field of the order to send an email for
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The order does not have an email address associated
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested order does not exist.
|
||||
:statuscode 503: The email could not be sent.
|
||||
|
||||
List of all order positions
|
||||
---------------------------
|
||||
|
||||
@@ -30,12 +30,12 @@ type string The expected ty
|
||||
* ``D`` – date
|
||||
* ``H`` – time
|
||||
* ``W`` – date and time
|
||||
required boolean If ``True``, the question needs to be filled out.
|
||||
required boolean If ``true``, the question needs to be filled out.
|
||||
position integer An integer, used for sorting
|
||||
items list of integers List of item IDs this question is assigned to.
|
||||
identifier string An arbitrary string that can be used for matching with
|
||||
other sources.
|
||||
ask_during_checkin boolean If ``True``, this question will not be asked while
|
||||
ask_during_checkin boolean If ``true``, this question will not be asked while
|
||||
buying the ticket, but will show up when redeeming
|
||||
the ticket instead.
|
||||
options list of objects In case of question type ``C`` or ``M``, this lists the
|
||||
@@ -46,6 +46,16 @@ options list of objects In case of ques
|
||||
├ identifier string An arbitrary string that can be used for matching with
|
||||
other sources.
|
||||
└ answer multi-lingual string The displayed value of this option
|
||||
dependency_question integer Internal ID of a different question. The current
|
||||
question will only be shown if the question given in
|
||||
this attribute is set to the value given in
|
||||
``dependency_value``. This cannot be combined with
|
||||
``ask_during_checkin``.
|
||||
dependency_value string The value ``dependency_question`` needs to be set to.
|
||||
If ``dependency_question`` is set to a boolean
|
||||
question, this should be ``"true"`` or ``"false"``.
|
||||
Otherwise, it should be the ``identifier`` of a
|
||||
question option.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
@@ -100,6 +110,8 @@ Endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -165,6 +177,8 @@ Endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -214,6 +228,8 @@ Endpoints
|
||||
"items": [1, 2],
|
||||
"position": 1,
|
||||
"ask_during_checkin": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"options": [
|
||||
{
|
||||
"answer": {"en": "S"}
|
||||
@@ -245,6 +261,8 @@ Endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -314,6 +332,8 @@ Endpoints
|
||||
"position": 2,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -45,6 +45,8 @@ meta_data dict Values set for
|
||||
|
||||
The ``event`` field has been added, together with filters on the list of dates and an organizer-level list.
|
||||
|
||||
.. versionchanged:: 2.6
|
||||
The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -103,11 +105,83 @@ Endpoints
|
||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param event: The ``slug`` field of the main event
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/
|
||||
|
||||
Creates a new subevent.
|
||||
|
||||
Permission required: "Can create events"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/events/sampleconf/subevents/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
|
||||
{
|
||||
"name": {"en": "First Sample Conference"},
|
||||
"active": false,
|
||||
"date_from": "2017-12-27T10:00:00Z",
|
||||
"date_to": null,
|
||||
"date_admission": null,
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"price": "12.00"
|
||||
}
|
||||
],
|
||||
"variation_price_overrides": [],
|
||||
"meta_data": {}
|
||||
}
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "First Sample Conference"},
|
||||
"active": false,
|
||||
"date_from": "2017-12-27T10:00:00Z",
|
||||
"date_to": null,
|
||||
"date_admission": null,
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"price": "12.00"
|
||||
}
|
||||
],
|
||||
"variation_price_overrides": [],
|
||||
"meta_data": {}
|
||||
}
|
||||
|
||||
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the main event
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The sub-event could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
||||
|
||||
Returns information on one sub-event, identified by its ID.
|
||||
@@ -149,13 +223,106 @@ Endpoints
|
||||
"meta_data": {}
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``slug`` field of the sub-event to fetch
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the main event
|
||||
:param id: The ``id`` field of the sub-event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
||||
|
||||
Updates a sub-event, identified by its ID. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to
|
||||
provide all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide
|
||||
the fields that you want to change.
|
||||
|
||||
Permission required: "Can change event settings"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content: application/json
|
||||
|
||||
{
|
||||
"name": {"en": "New Subevent Name"},
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"price": "23.42"
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "New Subevent Name"},
|
||||
"event": "sampleconf",
|
||||
"active": false,
|
||||
"date_from": "2017-12-27T10:00:00Z",
|
||||
"date_to": null,
|
||||
"date_admission": null,
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"price": "23.42"
|
||||
}
|
||||
],
|
||||
"variation_price_overrides": [],
|
||||
"meta_data": {}
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the main event
|
||||
:param id: The ``id`` field of the sub-event to update
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The sub-event could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/sub-event does not exist **or** you have no permission to update this resource.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
||||
|
||||
Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity.
|
||||
|
||||
Permission required: "Can change event settings"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the main event
|
||||
:param id: The ``id`` field of the sub-event to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/sub-event does not exist **or** you have no permission to delete this resource.
|
||||
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/subevents/
|
||||
|
||||
Returns a list of all sub-events of any event series you have access to within an organizer account.
|
||||
|
||||
@@ -18,8 +18,8 @@ max_usages integer The maximum num
|
||||
redeemed integer The number of times this voucher already has been
|
||||
redeemed.
|
||||
valid_until datetime The voucher expiration date (or ``null``).
|
||||
block_quota boolean If ``True``, quota is blocked for this voucher.
|
||||
allow_ignore_quota boolean If ``True``, this voucher can be redeemed even if a
|
||||
block_quota boolean If ``true``, quota is blocked for this voucher.
|
||||
allow_ignore_quota boolean If ``true``, this voucher can be redeemed even if a
|
||||
product is sold out and even if quota is not blocked
|
||||
for this voucher.
|
||||
price_mode string Determines how this voucher affects product prices.
|
||||
|
||||
@@ -17,11 +17,11 @@ The webhook resource contains the following public fields:
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the webhook
|
||||
enabled boolean If ``False``, this webhook will not receive any notifications
|
||||
enabled boolean If ``false``, this webhook will not receive any notifications
|
||||
target_url string The URL to call
|
||||
all_events boolean If ``True``, this webhook will receive notifications
|
||||
all_events boolean If ``true``, this webhook will receive notifications
|
||||
on all events of this organizer
|
||||
limit_events list of strings If ``all_events`` is ``False``, this is a list of
|
||||
limit_events list of strings If ``all_events`` is ``false``, this is a list of
|
||||
event slugs this webhook is active for
|
||||
action_types list of strings A list of action type filters that limit the
|
||||
notifications sent to this webhook. See below for
|
||||
|
||||
@@ -26,7 +26,7 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
|
||||
@@ -108,3 +108,43 @@ Endpoints
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/badgeitems/
|
||||
|
||||
Returns a list of all assignments of items to layouts
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/democon/badgeitems/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"layout": 2,
|
||||
"item": 3,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of a valid event
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
|
||||
@@ -316,7 +316,7 @@ uses to communicate with the pretix server.
|
||||
"total": 42,
|
||||
"version": 3,
|
||||
"event": {
|
||||
"name": "Demo Converence",
|
||||
"name": "Demo Conference",
|
||||
"slug": "democon",
|
||||
"date_from": "2016-12-27T17:00:00Z",
|
||||
"date_to": "2016-12-30T18:00:00Z",
|
||||
|
||||
@@ -114,3 +114,44 @@ Endpoints
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayoutitems/
|
||||
|
||||
Returns a list of all assignments of items to layouts
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/democon/ticketlayoutitems/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"layout": 2,
|
||||
"item": 3,
|
||||
"sales_channel": web
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of a valid event
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
|
||||
@@ -10,6 +10,7 @@ availabilities
|
||||
backend
|
||||
backends
|
||||
banktransfer
|
||||
Bcc
|
||||
boolean
|
||||
booleans
|
||||
cancelled
|
||||
@@ -94,6 +95,8 @@ renderers
|
||||
reportlab
|
||||
SaaS
|
||||
screenshot
|
||||
scss
|
||||
searchable
|
||||
selectable
|
||||
serializers
|
||||
serializers
|
||||
@@ -108,6 +111,7 @@ subdomains
|
||||
subevent
|
||||
subevents
|
||||
submodule
|
||||
subnet
|
||||
subpath
|
||||
Symfony
|
||||
systemd
|
||||
|
||||
@@ -21,11 +21,18 @@ Frontpage text
|
||||
your product types, give more information on the event or for other general notices.
|
||||
You can use :ref:`Markdown syntax <markdown-guide>` in this field.
|
||||
|
||||
Voucher explanation
|
||||
This text will be shown above the voucher input box. You can use it to explain how to obtain a voucher and use it.
|
||||
|
||||
Show variations of a product expanded by default
|
||||
If this is not checked, a product with variations will be shown as one row in the show by default and will expand
|
||||
into multiple rows once it is clicked on. With this box checked, the variations will be shown as multiple rows
|
||||
right from the beginning.
|
||||
|
||||
Ask search engines not to index the ticket shop
|
||||
If this is checked, we will set a HTML meta attribute asking search engines by Google not to put this ticket shop
|
||||
into their searchable index.
|
||||
|
||||
|
||||
The lower part of the page contains settings that you can **either** set on organizer-level for all your events **or**
|
||||
override for this single event individually. Those are:
|
||||
@@ -35,6 +42,12 @@ Primary color
|
||||
customers. We suggest not choosing something to light, since text in that color should be readable on a white
|
||||
background and white text should be readable on a background of this color.
|
||||
|
||||
Accent color for success
|
||||
This color will be used for success messages. We suggest to choose a dark shade of green.
|
||||
|
||||
Accent color for errors
|
||||
This color will be used for error messages. We suggest to choose a dark shade of red.
|
||||
|
||||
Font
|
||||
Choose one of multiple fonts to use for your web shop.
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ event.
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
The page is separated into three parts: "E-mail settings", "E-mail content" and "SMTP settings". We will explain all
|
||||
of them in detail on this page.
|
||||
The page is separated into four parts: "E-mail settings", "E-mail design", "E-mail content" and "SMTP settings".
|
||||
We will explain all of them in detail on this page.
|
||||
|
||||
E-mail settings
|
||||
---------------
|
||||
@@ -30,10 +30,18 @@ Signature
|
||||
This text will be appended to all e-mails in form of a signature. This might be useful e.g. to add your contact
|
||||
details or any legal information that needs to be included with the e-mails.
|
||||
|
||||
Bcc address
|
||||
This email address will receive a copy of every event-related email.
|
||||
|
||||
E-mail design
|
||||
-------------
|
||||
|
||||
In this part, you can choose and preview the layout of your emails. More layouts can be added by pretix plugins.
|
||||
|
||||
E-mail content
|
||||
--------------
|
||||
|
||||
The middle part of the page allows you to customize the exact texts of all e-mails sent by the system automatically.
|
||||
The next part of the page allows you to customize the exact texts of all e-mails sent by the system automatically.
|
||||
You can click on the different boxes to expand them and see the texts.
|
||||
|
||||
Within the texts, you can use placeholders that will later by replaced by values depending on the event or order. Below
|
||||
@@ -45,6 +53,7 @@ Placeholder Description
|
||||
============================== ===============================================================================
|
||||
event The event name
|
||||
total The order's total value
|
||||
total_with_currency The order's total value with a localized currency sign
|
||||
currency The currency used for the event (three-letter code)
|
||||
payment_info Information text specific to the payment method (e.g. banking details)
|
||||
url An URL pointing to the download/status page of the order
|
||||
@@ -112,6 +121,22 @@ Reminder to download tickets
|
||||
attendees to download their tickets. The e-mail should include a link to the ticket download. This e-mail will only
|
||||
ever be sent if you specify a number of days.
|
||||
|
||||
Order approval process
|
||||
If you configure one of your products to "require approval", orders of that product will not immediately be confirmed
|
||||
but only after you approved them manually. In this case, the following e-mail templates will be sent out.
|
||||
|
||||
Received order
|
||||
After an order has been received, this e-mail will be sent automatically instead of the "order placed" e-mail from
|
||||
above.
|
||||
|
||||
Approved order
|
||||
This e-mail will be sent after you manually approved an order. This should include instructions to pay for the order,
|
||||
which is why this will only be used for a paid order. For a free order, the "free order" e-mail from above will
|
||||
be sent.
|
||||
|
||||
Denied order
|
||||
This e-mail will be sent out to customers when their order has been denied.
|
||||
|
||||
SMTP settings
|
||||
-------------
|
||||
|
||||
|
||||
@@ -11,18 +11,6 @@ The settings at "Settings" → "Invoice" allow you to specify if and how pretix
|
||||
|
||||
In particular, you can configure the following things:
|
||||
|
||||
Ask for invoice address
|
||||
If this checkbox is enabled, customers will be able to enter an invoice address during checkout. If you only enable
|
||||
this box, the invoice address will be optional to fill in.
|
||||
|
||||
Require invoice address
|
||||
If this checkbox is enabled, entering an invoice address will be obligatory for all customers and it will not be
|
||||
able to create an order without entering an address.
|
||||
|
||||
Require customer name
|
||||
If this checkbox is enabled, the street, city, and country fields of the invoice address will still be optional but
|
||||
the name field will be obligatory.
|
||||
|
||||
Generate invoices
|
||||
This field controls whether pretix should generate an invoice for an order. You have the following options:
|
||||
|
||||
@@ -51,6 +39,51 @@ Attach invoices to emails
|
||||
"Automatically for all created orders" or to the payment confirmation e-mails if it is set to "Automatically on
|
||||
payment".
|
||||
|
||||
Invoice number prefix
|
||||
This is the prefix that will be prepended to all your invoice numbers. For example, if you set this to "Inv", your
|
||||
invoices will be numbered Inv00001, Inv00002, etc. If you leave this field empty, your event slug will be used,
|
||||
followed by a dash, e.g. DEMOCON-00001.
|
||||
|
||||
Within one organizer account, events with the same number prefix will share their number range. For example, if you
|
||||
set this to "Inv" for all of your events, there will be only one invoice numbered Inv00007 across all your events
|
||||
and the numbers will have gaps within one event.
|
||||
|
||||
Generate invoices with consecutive numbers
|
||||
If enabled, invoices will be created with numerical invoice numbers in the order of their creation, i.e.
|
||||
PREFIX-00001, PREFIX-00002, and so on. If disabled, invoice numbers will instead be generated from the order code,
|
||||
i.e. PREFIX-YHASD-1. When in doubt, keep this option enabled since it might be legally required in your country,
|
||||
but disabling it has the advantage that your customers can not estimate the number of tickets sold by looking at
|
||||
the invoice numbers.
|
||||
|
||||
Invoice language
|
||||
This setting allows you to configure the language of all invoices. You can either set it to one of your event
|
||||
language or dynamically to the language used by the customer.
|
||||
|
||||
Show free products on invoices
|
||||
If enabled, products that do not cost anything will still show up on invoices. Note that the order needs to contain
|
||||
at least one non-free product in order to generate an invoice.
|
||||
|
||||
Show attendee names on invoices
|
||||
If enabled, the attendee name will be printed on the invoice for admission tickets.
|
||||
|
||||
Ask for invoice address
|
||||
If this checkbox is enabled, customers will be able to enter an invoice address during checkout. If you only enable
|
||||
this box, the invoice address will be optional to fill in.
|
||||
|
||||
Require invoice address
|
||||
If this checkbox is enabled, entering an invoice address will be obligatory for all customers and it will not be
|
||||
able to create an order without entering an address.
|
||||
|
||||
Require customer name
|
||||
If this checkbox is enabled, the street, city, and country fields of the invoice address will still be optional but
|
||||
the name field will be obligatory.
|
||||
|
||||
Require a business address
|
||||
If enabled, the invoice address form will require a company name and do not allow personal addresses.
|
||||
|
||||
Ask for beneficiary
|
||||
If enabled, the invoice address form will contain an additional field to input the beneficiary of the transaction.
|
||||
|
||||
Ask for VAT ID
|
||||
If enabled, the invoice address form will not only ask for a postal address, but also for a VAT ID. The VAT ID will
|
||||
always be an optional field.
|
||||
@@ -62,26 +95,13 @@ Generate invoices with consecutive numbers
|
||||
but disabling it has the advantage that your customers can not estimate the number of tickets sold by looking at
|
||||
the invoice numbers.
|
||||
|
||||
Invoice number prefix
|
||||
This is the prefix that will be prepended to all your invoice numbers. For example, if you set this to "Inv", your
|
||||
invoices will be numbered Inv00001, Inv00002, etc. If you leave this field empty, your event slug will be used,
|
||||
followed by a dash, e.g. DEMOCON-00001.
|
||||
|
||||
Within one organizer account, events with the same number prefix will share their number range. For example, if you
|
||||
set this to "Inv" for all of your events, there will be only one invoice numbered Inv00007 across all your events
|
||||
and the numbers will have gaps within one event.
|
||||
|
||||
Show free products on invoices
|
||||
If enabled, products that do not cost anything will still show up on invoices. Note that the order needs to contain
|
||||
at least one non-free product in order to generate an invoice.
|
||||
|
||||
Show attendee names on invoices
|
||||
If enabled, the attendee name will be printed on the invoice for admission tickets.
|
||||
|
||||
Your address
|
||||
This should be set to the address of the entity issuing the invoice (read: you) and will be printed inside
|
||||
Your invoice details
|
||||
These fields should be set to the address of the entity issuing the invoice (read: you) and will be printed inside
|
||||
the header of the invoice.
|
||||
|
||||
Invoice style
|
||||
This setting allows you to choose the design of your invoice. Additional designs can be added by pretix plugins.
|
||||
|
||||
Introductory text
|
||||
A free custom text that will be printed above the list of products on the invoice.
|
||||
|
||||
|
||||
260
doc/user/events/structureguide.rst
Normal file
260
doc/user/events/structureguide.rst
Normal file
@@ -0,0 +1,260 @@
|
||||
Product structure guide
|
||||
=======================
|
||||
|
||||
Between products, categories, variations, add-ons, bundles, and quotas, pretix provides a wide range of features that allow you to set up your event in the way you want it.
|
||||
However, it is easy to get lost in the process or to get started with building your event right.
|
||||
Often times, there are multiple ways to do something that come with different advantages and disadvantages.
|
||||
This guide will walk you through a number of typical examples of pretix event structures and will explain how to set them up – feel free to just skip ahead to a section relevant for you.
|
||||
|
||||
Terminology
|
||||
-----------
|
||||
|
||||
Products
|
||||
A product is a basic entity that can be bought. You can think of it as a ticket type, but it can be more things than just a ticket, it can also be a piece of merchandise, a parking slot, etc.
|
||||
You might find some places where they are called "items" instead, but we're trying to get rid of that.
|
||||
|
||||
Product categories
|
||||
Products can be sorted into categories. Each product can only be in one category. Category are mostly used for grouping related products together to make your event page easier to read for buyers. However, we'll need categories as well to set up some of the structures outlined below.
|
||||
|
||||
Product variations
|
||||
During creation of a product, you can decide that your product should have multiple variations. Variations of a product can differ in price, description, and availability, but are otherwise the same. You could use this e.g. for differentiating between a regular ticket and a discounted ticket for students, or when selling merchandise to differentiate the different sizes of a t-shirt.
|
||||
|
||||
Product add-ons
|
||||
Add-ons are products that are sold together with another product (which we will call the base product in this case). For example, you could have a base product "Conference ticket" and then define multiple workshops that can be chosen as an add-on.
|
||||
|
||||
Product bundles
|
||||
Bundles work very similarly to add-ons, but are different in the way that they are always automatically included with the base product and cannot be optional. In contrast to add-on products, the same product can be included multiple times in a bundle.
|
||||
|
||||
Quotas
|
||||
Quotas define the availability of products. A quota has a size (i.e. the number of products in the inventory) and is mapped to one or multiple products or variations.
|
||||
|
||||
Questions
|
||||
Questions are user-defined form fields that buyers will need to fill out when purchasing a product.
|
||||
|
||||
Use case: Multiple price levels
|
||||
-------------------------------
|
||||
|
||||
Imagine you're running a concert with general admission that sells a total of 200 tickets for two prices:
|
||||
|
||||
* Regular: € 25.00
|
||||
* Students: € 19.00
|
||||
|
||||
You can either set up two different products called e.g. "Regular ticket" and "Student ticket" with the respective prices, or two variations within the same product. In this simple case, it really doesn't matter.
|
||||
|
||||
In addition, you will need quotas. If you do not care how many of your tickets are sold to students, you should set up just **one quota** of 200 called e.g. "General admission" that you link to **both products**.
|
||||
|
||||
If you want to limit the number of student tickets to 50 to ensure a certain minimum revenue, but do not want to limit the number of regular tickets artificially, we suggest you to create the same quota of 200 that is linked to both products, and then create a **second quota** of 50 that is only linked to the student ticket. This way, the system will reduce both quotas whenever a student ticket is sold and only the larger quota when a regular ticket is sold.
|
||||
|
||||
Use case: Early-bird tiers
|
||||
--------------------------
|
||||
|
||||
Let's say you run a conference that has the following pricing scheme:
|
||||
|
||||
* 12 to 6 months before the event: € 450
|
||||
* 6 to 3 months before the event: € 550
|
||||
* closer than 3 months to the event: € 650
|
||||
|
||||
Of course, you could just set up one product and change its price at the given dates manually, but if you want to set this up automatically, here's how:
|
||||
|
||||
Create three products (e.g. "super early bird", "early bird", "regular ticket") with the respective prices and one shared quota of your total event capacity. Then, set the **available from** and **available until** configuration fields of the products to automatically turn them on and off based on the current date.
|
||||
|
||||
.. note::
|
||||
|
||||
pretix currently can't do early-bird tiers based on **ticket number** instead of time. We're planning this feature for later in 2019. For now, you'll need to monitor that manually.
|
||||
|
||||
Use case: Up-selling of ticket extras
|
||||
-------------------------------------
|
||||
|
||||
Let's assume you're putting up a great music festival, and to save trouble with handling payments on-site, you want to sell parking spaces together with your ticket. By using our add-on feature, you can prompt all users to book the parking space (to make sure they see it) and ensure that only people with a ticket can book a parking space. You can set it up like this:
|
||||
|
||||
* Create a base product "Festival admission"
|
||||
* Create a quota for the base product
|
||||
* Create a category "Ticket extras" and check "Products in this category are add-on products"
|
||||
* Create a product "Parking space" within that category
|
||||
* Create a quota for the parking space product
|
||||
* Go to the base product and select the tab "Add-Ons" at the top. Click "Add a new add-on" and choose the "Ticket extras" category. You can keep the numbers at 0 and 1.
|
||||
|
||||
During checkout, all buyers of the base product will now be prompted if they want to add the parking space.
|
||||
|
||||
.. tip::
|
||||
|
||||
You can also use add-on products for free things, just to keep tabs on capacity.
|
||||
|
||||
Use case: Conference with workshops
|
||||
-----------------------------------
|
||||
|
||||
When running a conference, you might also organize a number of workshops with smaller capacity. To be able to plan, it would be great to know which workshops an attendee plans to attend.
|
||||
|
||||
Your first and simplest option is to just create a multiple-choice question. This has the upside of making it easy for users to change their mind later on, but will not allow you to restrict the number of attendees signing up for a given workshop – or even charge extra for a given workshop.
|
||||
|
||||
The usually better option is to go with add-on products. Let's take for example the following conference schedule, in which the lecture can be attended by anyone, but the workshops only have space for 20 persons each:
|
||||
|
||||
==================== =================================== ===================================
|
||||
Time Room A Room B
|
||||
==================== =================================== ===================================
|
||||
Wednesday morning Lecture
|
||||
Wednesday afternoon Workshop A Workshop B
|
||||
Thursday morning Workshop C Workshop D (20 € extra charge)
|
||||
==================== =================================== ===================================
|
||||
|
||||
Assuming you already created one or more products for your general conference admission, we suggest that you additionally create:
|
||||
|
||||
* A category called "Workshops" with the checkbox "Products in this category are add-on products" activated
|
||||
|
||||
* A free product called "Wednesday afternoon" within the category "Workshops" and with two variations:
|
||||
|
||||
* Workshop A
|
||||
|
||||
* Workshop B
|
||||
|
||||
* A free product called "Thursday morning" within the category "Workshops" and with two variations:
|
||||
|
||||
* Workshop C
|
||||
|
||||
* Workshop D with a price of 20 €
|
||||
|
||||
* Four quotas for each of the workshops
|
||||
|
||||
* One add-on configuration on your base product that allows users to choose between 0 and 2 products from the category "Workshops"
|
||||
|
||||
Use case: Discounted packages
|
||||
-----------------------------
|
||||
|
||||
Imagine you run a trade show that opens on three consecutive days and you want to have the following pricing:
|
||||
|
||||
* Single day: € 10
|
||||
* Any two days: € 17
|
||||
* All three days: € 25
|
||||
|
||||
In this case, there are multiple different ways you could set this up with pretix.
|
||||
|
||||
Option A: Combination products
|
||||
""""""""""""""""""""""""""""""
|
||||
|
||||
With this option, you just set up all the different combinations someone could by as a separate product. In this case, you would need 7 products:
|
||||
|
||||
* Day 1 pass
|
||||
* Day 2 pass
|
||||
* Day 3 pass
|
||||
* Day 1+2 pass
|
||||
* Day 2+3 pass
|
||||
* Day 1+3 pass
|
||||
* All-day pass
|
||||
|
||||
Then, you create three quotas, each one with the maximum capacity of your venue on any given day:
|
||||
|
||||
* Day 1 quota, linked to "Day 1 pass", "Day 1+2 pass", "Day 1+3 pass", and "All-day pass"
|
||||
* Day 2 quota, linked to "Day 2 pass", "Day 1+2 pass", "Day 2+3 pass", and "All-day pass"
|
||||
* Day 3 quota, linked to "Day 3 pass", "Day 2+3 pass", "Day 1+3 pass", and "All-day pass"
|
||||
|
||||
This way, every person gets exactly one ticket that they can use for all days that they attend. You can later set up check-in lists appropriately to make sure only tickets valid for a certain day can be scanned on that day.
|
||||
|
||||
The benefit of this option is that your product structure and order structure stays very simple. However, the two-day packages scale badly when you need many products.
|
||||
|
||||
We recommend this setup for most setups in which the number of possible combinations does not exceed the number of parts (here: number of days) by much.
|
||||
|
||||
Option B: Add-ons and bundles
|
||||
"""""""""""""""""""""""""""""
|
||||
|
||||
We can combine the two features "product add-ons" and "product bundles" to set this up in a different way. Here, you would create the following five products:
|
||||
|
||||
* Day 1 pass in a category called "Day passes"
|
||||
* Day 2 pass in a category called "Day passes"
|
||||
* Day 3 pass in a category called "Day passes"
|
||||
* Two-day pass
|
||||
* All-day pass
|
||||
|
||||
This time, you will need five quotas:
|
||||
|
||||
* Day 1 quota, linked to "Day 1 pass"
|
||||
* Day 2 quota, linked to "Day 2 pass"
|
||||
* Day 3 quota, linked to "Day 3 pass"
|
||||
* Two-day pass quota, linked to "Two-day pass" (can be unlimited)
|
||||
* All-day pass quota, linked to "All-day pass" (can be unlimited)
|
||||
|
||||
Then, you open the "Add-On" tab in the settings of the **Two-day pass** product and create a new add-on configuration specifying the following options:
|
||||
|
||||
* Category: "Day passes"
|
||||
* Minimum number: 2
|
||||
* Maximum number: 2
|
||||
* Add-Ons are included in the price: Yes
|
||||
|
||||
This way, when buying a two-day pass, the user will be able to select *exactly* two days for free, which will then be added to the cart. Depending on your specific configuration, the user will now receive *two separate* tickets, one for each day.
|
||||
|
||||
For the all-day pass, you open the "Bundled products" tab in the settings of the **All-day pass** product and add **three** new bundled items with the following options:
|
||||
|
||||
* Bundled product: "Day 1/2/3"
|
||||
* Bundled variation: None
|
||||
* Count: 1
|
||||
* Designated price: 0
|
||||
|
||||
This way, when buying an all-day pass, three free day passes will *automatically* be added to the cart. Depending on your specific configuration, the user will now receive *three separate* tickets, one for each day.
|
||||
|
||||
This approach makes your order data more complicated, since e.g. someone who buys an all-day pass now technically bought **four products**. However, this option allows for more flexibility when you have lots of options to choose from.
|
||||
|
||||
.. tip::
|
||||
|
||||
Depending on the packages you offer, you **might not need both the add-on and the bundle feature**, i.e. you only need the add-on feature for the two-day pass and only the bundle feature for the all-day pass. You could also set up the two-day pass like we showed here, but the all-day pass like in option A!
|
||||
|
||||
Use case: Group discounts
|
||||
-------------------------
|
||||
|
||||
Often times, you want to give discounts for whole groups attending your event. pretix can't automatically discount based on volume, but there's still some ways you can set up group tickets.
|
||||
|
||||
Flexible group sizes
|
||||
""""""""""""""""""""
|
||||
|
||||
If you want to give out discounted tickets to groups starting at a given size, but still billed per person, you can do so by creating a special **Group ticket** at the per-person price and set the **Minimum amount per order** option of the ticket to the minimal group size.
|
||||
|
||||
This way, your ticket can be bought an arbitrary number of times – but no less than the given minimal amount per order.
|
||||
|
||||
Fixed group sizes
|
||||
"""""""""""""""""
|
||||
|
||||
If you want to sell group tickets in fixed sizes, e.g. a table of eight at your gala dinner, you can use product bundles. Assuming you already set up a ticket for admission of single persons, you then set up a second product **Table (8 persons)** with a discounted full price. Then, head to the **Bundled products** tab of that product and add one bundle configuration to include the single admission product **eight times**. Next, create an unlimited quota mapped to the new product.
|
||||
|
||||
This way, the purchase of a table will automatically create eight tickets, leading to a correct calculation of your total quota and, as expected, eight persons on your check-in list. You can even ask for the individual names of the persons during checkout.
|
||||
|
||||
Use case: Restricted audience
|
||||
-----------------------------
|
||||
|
||||
Not all events are for everyone. Sometimes, there is a good reason to restrict access to your event or parts of your event only to a specific, invited group. There's two ways to implement this with pretix:
|
||||
|
||||
Option A: Required voucher codes
|
||||
""""""""""""""""""""""""""""""""
|
||||
|
||||
If you check the option "**This product can only be bought using a voucher**" of one or multiple products, only people holding an applicable voucher code will be able to buy the product.
|
||||
|
||||
You can then generate voucher codes for the respective product and send them out to the group of possible attendees. If the recipients should still be able to choose between different products, you can create an additional quota and map the voucher to that quota instead of the products themselves.
|
||||
|
||||
There's also the second option "**This product will only be shown if a voucher matching the product is redeemed**". In this case, the existence of the product won't even be shown before a voucher code is entered – useful for a VIP option in a shop where you also sell other products to the general public. Please note that this option does **not** work with vouchers assigned to a quota, only with vouchers assigned directly to the product.
|
||||
|
||||
This option is appropriate if you know the group of people beforehand, e.g. members of a club, and you can mail them their access codes.
|
||||
|
||||
Option B: Order approvals
|
||||
"""""""""""""""""""""""""
|
||||
|
||||
If you do not know your audience already, but still want to restrict it to a certain group, e.g. people with a given profession, you can check the "**Buying this product requires approval**" in the settings of your product. If a customer tries to buy such a product, they will be able to place their order but can not proceed to payment. Instead, you will be asked to approve or deny the order and only if you approve it, we will send a payment link to the customer.
|
||||
|
||||
This requires the customer to interact with the ticket shop twice (once for the order, once for the payment) which adds a little more friction, but gives you full control over who attends the event.
|
||||
|
||||
Use case: Mixed taxation
|
||||
------------------------
|
||||
|
||||
Let's say you are a charitable organization in Germany and are allowed to charge a reduced tax rate of 7% for your educational event. However, your event includes a significant amount of food, you might need to charge a 19% tax rate on that portion. For example, your desired tax structure might then look like this:
|
||||
|
||||
* Conference ticket price: € 450 (incl. € 150 for food)
|
||||
|
||||
* incl. € 19.63 VAT at 7%
|
||||
* incl. € 23.95 VAT at 19%
|
||||
|
||||
You can implement this in pretix using product bundles. In order to do so, you should create the following two products:
|
||||
|
||||
* Conference ticket at € 450 with a 7% tax rule
|
||||
* Conference food at € 150 with a 19% tax rule and the option "**Only sell this product as part of a bundle**" set
|
||||
|
||||
In addition to your normal conference quota, you need to create an unlimited quota for the food product.
|
||||
|
||||
Then, head to the **Bundled products** tab of the "conference ticket" and add the "conference food" as a bundled product with a **designated price** of € 150.
|
||||
|
||||
Once a customer tries to buy the € 450 conference ticket, a sub-product will be added and the price will automatically be split into the two components, leading to a correct computation of taxes.
|
||||
@@ -25,6 +25,10 @@ Generate tickets for non-admission products
|
||||
By default, tickets will only be generated for products that are marked as admission products. Enable this option to
|
||||
generate tickets for all products instead.
|
||||
|
||||
Offer to download tickets even before an order is paid
|
||||
By default, ticket download is only possible for paid orders. If you run an event where people usually pay only after
|
||||
the event, you can check this box to enable ticket download even before.
|
||||
|
||||
Below these settings, the detail settings for the various ticket file formats are offered. They differ from format to
|
||||
format and only share the common "Enable" setting that can be used to turn them on. By default, pretix ships with
|
||||
a PDF output plugin that you can configure through a visual design editor.
|
||||
@@ -114,6 +114,35 @@ If you want to disable voucher input in the widget, you can pass the ``disable-v
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" disable-vouchers></pretix-widget>
|
||||
|
||||
Multi-event selection
|
||||
---------------------
|
||||
|
||||
If you want to embed multiple events in a single widget, you can do so. If it's multiple dates of an event series, just leave off the ``series`` attribute::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/series/"></pretix-widget>
|
||||
|
||||
If you want to include all your public events, you can just reference your organizer::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/"></pretix-widget>
|
||||
|
||||
There is an optional ``style`` parameter that let's you choose between a calendar view and a list view. If you do not set it, the choice will be taken from your organizer settings::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/series/" style="list"></pretix-widget>
|
||||
<pretix-widget event="https://pretix.eu/demo/series/" style="calendar"></pretix-widget>
|
||||
|
||||
You can see an example here:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/series/" style="calendar"></pretix-widget>
|
||||
<noscript>
|
||||
<div class="pretix-widget">
|
||||
<div class="pretix-widget-info-message">
|
||||
JavaScript is disabled in your browser. To access our ticket shop without javascript, please <a target="_blank" href="https://pretix.eu/demo/series/">click here</a>.
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
pretix Button
|
||||
-------------
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ wanting to use pretix to sell tickets.
|
||||
organizers/index
|
||||
events/create
|
||||
events/settings
|
||||
events/structureguide
|
||||
events/widget
|
||||
faq
|
||||
markdown
|
||||
@@ -3,6 +3,13 @@
|
||||
PayPal
|
||||
======
|
||||
|
||||
.. note::
|
||||
|
||||
If you use pretix Hosted, you do not longer need to go through this tedious process! You can
|
||||
just open the PayPal settings in the payment section of your event, click "Connect to PayPal"
|
||||
and log in to your PayPal account. The following guide is only required for self-hosted
|
||||
versions of pretix.
|
||||
|
||||
To integrate PayPal with pretix, you first need to have an active PayPal merchant account. If you do not already have a
|
||||
PayPal account, you can create one on `paypal.com`_.
|
||||
If you look into pretix' settings, you are required to fill in two keys:
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.5.0.dev0"
|
||||
__version__ = "2.6.0.dev0"
|
||||
|
||||
@@ -191,8 +191,8 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class SubEventSerializer(I18nAwareModelSerializer):
|
||||
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True)
|
||||
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True)
|
||||
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True, required=False)
|
||||
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True, required=False)
|
||||
event = SlugRelatedField(slug_field='slug', read_only=True)
|
||||
meta_data = MetaDataField(source='*')
|
||||
|
||||
@@ -202,6 +202,103 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
'presale_start', 'presale_end', 'location', 'event',
|
||||
'item_price_overrides', 'variation_price_overrides', 'meta_data')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
event = self.context['request'].event
|
||||
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
Event.clean_dates(data.get('date_from'), data.get('date_to'))
|
||||
Event.clean_presale(data.get('presale_start'), data.get('presale_end'))
|
||||
|
||||
SubEvent.clean_items(event, [item['item'] for item in full_data.get('subeventitem_set')])
|
||||
SubEvent.clean_variations(event, [item['variation'] for item in full_data.get('subeventitemvariation_set')])
|
||||
return data
|
||||
|
||||
def validate_item_price_overrides(self, data):
|
||||
return list(filter(lambda i: 'item' in i, data))
|
||||
|
||||
def validate_variation_price_overrides(self, data):
|
||||
return list(filter(lambda i: 'variation' in i, data))
|
||||
|
||||
@cached_property
|
||||
def meta_properties(self):
|
||||
return {
|
||||
p.name: p for p in self.context['request'].organizer.meta_properties.all()
|
||||
}
|
||||
|
||||
def validate_meta_data(self, value):
|
||||
for key in value['meta_data'].keys():
|
||||
if key not in self.meta_properties:
|
||||
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
|
||||
return value
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
||||
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
subevent = super().create(validated_data)
|
||||
|
||||
for item_price_override_data in item_price_overrides_data:
|
||||
SubEventItem.objects.create(subevent=subevent, **item_price_override_data)
|
||||
for variation_price_override_data in variation_price_overrides_data:
|
||||
SubEventItemVariation.objects.create(subevent=subevent, **variation_price_override_data)
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
subevent.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
|
||||
return subevent
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, instance, validated_data):
|
||||
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
|
||||
variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {}
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
subevent = super().update(instance, validated_data)
|
||||
|
||||
existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)}
|
||||
|
||||
for item_price_override_data in item_price_overrides_data:
|
||||
id = existing_item_overrides.pop(item_price_override_data['item'], None)
|
||||
SubEventItem(id=id, subevent=subevent, **item_price_override_data).save()
|
||||
|
||||
SubEventItem.objects.filter(id__in=existing_item_overrides.values()).delete()
|
||||
|
||||
existing_variation_overrides = {item.variation: item.id for item in SubEventItemVariation.objects.filter(subevent=subevent)}
|
||||
|
||||
for variation_price_override_data in variation_price_overrides_data:
|
||||
id = existing_variation_overrides.pop(variation_price_override_data['variation'], None)
|
||||
SubEventItemVariation(id=id, subevent=subevent, **variation_price_override_data).save()
|
||||
|
||||
SubEventItemVariation.objects.filter(id__in=existing_variation_overrides.values()).delete()
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
current = {mv.property: mv for mv in subevent.meta_values.select_related('property')}
|
||||
for key, value in meta_data.items():
|
||||
prop = self.meta_properties.get(key)
|
||||
if prop in current:
|
||||
current[prop].value = value
|
||||
current[prop].save()
|
||||
else:
|
||||
subevent.meta_values.create(
|
||||
property=self.meta_properties.get(key),
|
||||
value=value
|
||||
)
|
||||
|
||||
for prop, current_object in current.items():
|
||||
if prop.name not in meta_data:
|
||||
current_object.delete()
|
||||
|
||||
return subevent
|
||||
|
||||
|
||||
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
|
||||
@@ -7,12 +7,15 @@ from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
|
||||
Quota,
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
|
||||
QuestionOption, Quota,
|
||||
)
|
||||
|
||||
|
||||
class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10,
|
||||
coerce_to_string=True)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
@@ -20,12 +23,22 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10,
|
||||
coerce_to_string=True)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price')
|
||||
|
||||
|
||||
class InlineItemBundleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemBundle
|
||||
fields = ('bundled_item', 'bundled_variation', 'count',
|
||||
'designated_price')
|
||||
|
||||
|
||||
class InlineItemAddOnSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemAddOn
|
||||
@@ -33,6 +46,31 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
|
||||
'position', 'price_included')
|
||||
|
||||
|
||||
class ItemBundleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemBundle
|
||||
fields = ('id', 'bundled_item', 'bundled_variation', 'count',
|
||||
'designated_price')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
event = self.context['event']
|
||||
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
ItemBundle.clean_itemvar(event, full_data.get('bundled_item'), full_data.get('bundled_variation'))
|
||||
|
||||
item = self.context['item']
|
||||
if item == full_data.get('bundled_item'):
|
||||
raise ValidationError(_("The bundled item must not be the same item as the bundling one."))
|
||||
if full_data.get('bundled_item'):
|
||||
if full_data['bundled_item'].bundles.exists():
|
||||
raise ValidationError(_("The bundled item must not have bundles on its own."))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ItemAddOnSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemAddOn
|
||||
@@ -69,6 +107,7 @@ class ItemTaxRateField(serializers.Field):
|
||||
|
||||
class ItemSerializer(I18nAwareModelSerializer):
|
||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||
|
||||
@@ -77,9 +116,9 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
||||
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
|
||||
'position', 'picture', 'available_from', 'available_until',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
|
||||
'variations', 'addons', 'original_price', 'require_approval', 'generate_tickets')
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets')
|
||||
read_only_fields = ('has_variations', 'picture')
|
||||
|
||||
def get_serializer_context(self):
|
||||
@@ -87,8 +126,8 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
if self.instance and ('addons' in data or 'variations' in data):
|
||||
raise ValidationError(_('Updating add-ons or variations via PATCH/PUT is not supported. Please use the '
|
||||
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data):
|
||||
raise ValidationError(_('Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use the '
|
||||
'dedicated nested endpoint.'))
|
||||
|
||||
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
|
||||
@@ -104,6 +143,12 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
Item.clean_tax_rule(value, self.context['event'])
|
||||
return value
|
||||
|
||||
def validate_bundles(self, value):
|
||||
if not self.instance:
|
||||
for b_data in value:
|
||||
ItemBundle.clean_itemvar(self.context['event'], b_data['bundled_item'], b_data['bundled_variation'])
|
||||
return value
|
||||
|
||||
def validate_addons(self, value):
|
||||
if not self.instance:
|
||||
for addon_data in value:
|
||||
@@ -117,11 +162,14 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
def create(self, validated_data):
|
||||
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
|
||||
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
||||
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
|
||||
item = Item.objects.create(**validated_data)
|
||||
for variation_data in variations_data:
|
||||
ItemVariation.objects.create(item=item, **variation_data)
|
||||
for addon_data in addons_data:
|
||||
ItemAddOn.objects.create(base_item=item, **addon_data)
|
||||
for bundle_data in bundles_data:
|
||||
ItemBundle.objects.create(base_item=item, **bundle_data)
|
||||
return item
|
||||
|
||||
|
||||
@@ -159,12 +207,20 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'identifier')
|
||||
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_value')
|
||||
|
||||
def validate_identifier(self, value):
|
||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||
return value
|
||||
|
||||
def validate_dependency_question(self, value):
|
||||
if value:
|
||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||
if value == self.instance:
|
||||
raise ValidationError('A question cannot depend on itself.')
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
if self.instance and 'options' in data:
|
||||
@@ -176,6 +232,18 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
|
||||
raise ValidationError('Dependencies are not supported during check-in.')
|
||||
|
||||
dep = full_data.get('dependency_question')
|
||||
if dep:
|
||||
seen_ids = {self.instance.pk} if self.instance else set()
|
||||
while dep:
|
||||
if dep.pk in seen_ids:
|
||||
raise ValidationError(_('Circular dependency between questions detected.'))
|
||||
seen_ids.add(dep.pk)
|
||||
dep = dep.dependency_question
|
||||
|
||||
Question.clean_items(event, full_data.get('items'))
|
||||
return data
|
||||
|
||||
|
||||
@@ -220,26 +220,73 @@ class OrderRefundSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class OrderSerializer(I18nAwareModelSerializer):
|
||||
invoice_address = InvoiceAddressSerializer()
|
||||
positions = OrderPositionSerializer(many=True)
|
||||
fees = OrderFeeSerializer(many=True)
|
||||
downloads = OrderDownloadsField(source='*')
|
||||
payments = OrderPaymentSerializer(many=True)
|
||||
refunds = OrderRefundSerializer(many=True)
|
||||
payment_date = OrderPaymentDateField(source='*')
|
||||
payment_provider = OrderPaymentTypeField(source='*')
|
||||
invoice_address = InvoiceAddressSerializer(allow_null=True)
|
||||
positions = OrderPositionSerializer(many=True, read_only=True)
|
||||
fees = OrderFeeSerializer(many=True, read_only=True)
|
||||
downloads = OrderDownloadsField(source='*', read_only=True)
|
||||
payments = OrderPaymentSerializer(many=True, read_only=True)
|
||||
refunds = OrderRefundSerializer(many=True, read_only=True)
|
||||
payment_date = OrderPaymentDateField(source='*', read_only=True)
|
||||
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel')
|
||||
fields = (
|
||||
'code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
||||
)
|
||||
read_only_fields = (
|
||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'positions', 'downloads',
|
||||
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
|
||||
self.fields['positions'].child.fields.pop('pdf_data')
|
||||
|
||||
def validate_locale(self, l):
|
||||
if l not in set(k for k in self.instance.event.settings.locales):
|
||||
raise ValidationError('"{}" is not a supported locale for this event.'.format(l))
|
||||
return l
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||
update_fields = ['comment', 'checkin_attention', 'email', 'locale']
|
||||
print(validated_data)
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
|
||||
if not iadata:
|
||||
try:
|
||||
instance.invoice_address.delete()
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
name = iadata.pop('name', '')
|
||||
if name and not iadata.get('name_parts'):
|
||||
iadata['name_parts'] = {
|
||||
'_legacy': name
|
||||
}
|
||||
try:
|
||||
ia = instance.invoice_address
|
||||
if iadata.get('vat_id') != ia.vat_id:
|
||||
ia.vat_id_validated = False
|
||||
self.fields['invoice_address'].update(ia, iadata)
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
InvoiceAddress.objects.create(order=instance, **iadata)
|
||||
|
||||
for attr, value in validated_data.items():
|
||||
if attr in update_fields:
|
||||
setattr(instance, attr, value)
|
||||
|
||||
instance.save(update_fields=update_fields)
|
||||
return instance
|
||||
|
||||
|
||||
class AnswerCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ question_router.register(r'options', item.QuestionOptionViewSet)
|
||||
item_router = routers.DefaultRouter()
|
||||
item_router.register(r'variations', item.ItemVariationViewSet)
|
||||
item_router.register(r'addons', item.ItemAddOnViewSet)
|
||||
item_router.register(r'bundles', item.ItemBundleViewSet)
|
||||
|
||||
order_router = routers.DefaultRouter()
|
||||
order_router.register(r'payments', order.PaymentViewSet)
|
||||
|
||||
@@ -217,9 +217,10 @@ class SubEventFilter(FilterSet):
|
||||
return queryset.exclude(expr)
|
||||
|
||||
|
||||
class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
|
||||
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = SubEventSerializer
|
||||
queryset = ItemCategory.objects.none()
|
||||
write_permission = 'can_change_event_settings'
|
||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||
filterset_class = SubEventFilter
|
||||
|
||||
@@ -240,6 +241,42 @@ class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
|
||||
'subeventitem_set', 'subeventitemvariation_set'
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
super().perform_update(serializer)
|
||||
|
||||
serializer.instance.log_action(
|
||||
'pretix.subevent.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.subevent.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied('The sub-event can not be deleted as it has already been used in orders. Please set'
|
||||
' \'active\' to false instead to hide it from users.')
|
||||
try:
|
||||
with transaction.atomic():
|
||||
instance.log_action(
|
||||
'pretix.subevent.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
except ProtectedError:
|
||||
raise PermissionDenied('The sub-event could not be deleted as some constraints (e.g. data created by '
|
||||
'plug-ins) do not allow it.')
|
||||
|
||||
|
||||
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = TaxRuleSerializer
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
@@ -9,14 +10,14 @@ from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.serializers.item import (
|
||||
ItemAddOnSerializer, ItemCategorySerializer, ItemSerializer,
|
||||
ItemVariationSerializer, QuestionOptionSerializer, QuestionSerializer,
|
||||
QuotaSerializer,
|
||||
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
||||
ItemSerializer, ItemVariationSerializer, QuestionOptionSerializer,
|
||||
QuestionSerializer, QuotaSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
|
||||
Quota,
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
|
||||
QuestionOption, Quota,
|
||||
)
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
@@ -46,7 +47,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.items.select_related('tax_rule').prefetch_related('variations', 'addons').all()
|
||||
return self.request.event.items.select_related('tax_rule').prefetch_related('variations', 'addons', 'bundles').all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
@@ -96,17 +97,20 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
@cached_property
|
||||
def item(self):
|
||||
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
|
||||
def get_queryset(self):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
return item.variations.all()
|
||||
return self.item.variations.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['item'] = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
ctx['item'] = self.item
|
||||
return ctx
|
||||
|
||||
def perform_create(self, serializer):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
item = self.item
|
||||
if not item.has_variations:
|
||||
raise PermissionDenied('This variation cannot be created because the item does not have variations. '
|
||||
'Changing a product without variations to a product with variations is not allowed.')
|
||||
@@ -149,6 +153,58 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
|
||||
class ItemBundleViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ItemBundleSerializer
|
||||
queryset = ItemBundle.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||
ordering_fields = ('id',)
|
||||
ordering = ('id',)
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
@cached_property
|
||||
def item(self):
|
||||
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.item.bundles.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['item'] = self.item
|
||||
return ctx
|
||||
|
||||
def perform_create(self, serializer):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
serializer.save(base_item=item)
|
||||
item.log_action(
|
||||
'pretix.event.item.bundles.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.base_item.log_action(
|
||||
'pretix.event.item.bundles.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
super().perform_destroy(instance)
|
||||
instance.base_item.log_action(
|
||||
'pretix.event.item.bundles.removed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'bundled_item': instance.bundled_item.pk, 'bundled_variation': instance.bundled_variation.pk if instance.bundled_variation else None,
|
||||
'count': instance.count, 'designated_price': instance.designated_price}
|
||||
)
|
||||
|
||||
|
||||
class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ItemAddOnSerializer
|
||||
queryset = ItemAddOn.objects.none()
|
||||
@@ -158,18 +214,21 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
@cached_property
|
||||
def item(self):
|
||||
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
|
||||
def get_queryset(self):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
return item.addons.all()
|
||||
return self.item.addons.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['item'] = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
ctx['item'] = self.item
|
||||
return ctx
|
||||
|
||||
def perform_create(self, serializer):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
item = self.item
|
||||
category = get_object_or_404(ItemCategory, pk=self.request.data['addon_category'])
|
||||
serializer.save(base_item=item, addon_category=category)
|
||||
item.log_action(
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.db.models.functions import Coalesce, Concat
|
||||
from django.http import FileResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import mixins, serializers, status, viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
@@ -16,7 +17,7 @@ from rest_framework.exceptions import (
|
||||
APIException, NotFound, PermissionDenied, ValidationError,
|
||||
)
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin
|
||||
from rest_framework.mixins import CreateModelMixin
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
@@ -28,6 +29,7 @@ from pretix.api.serializers.order import (
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, Order,
|
||||
OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
|
||||
generate_position_secret, generate_secret,
|
||||
)
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services.invoices import (
|
||||
@@ -54,7 +56,7 @@ class OrderFilter(FilterSet):
|
||||
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
|
||||
|
||||
|
||||
class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class OrderViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = OrderSerializer
|
||||
queryset = Order.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
@@ -308,6 +310,70 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def create_invoice(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
has_inv = order.invoices.exists() and not (
|
||||
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
||||
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
|
||||
)
|
||||
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'True') or not invoice_qualified(order):
|
||||
return Response(
|
||||
{'detail': _('You cannot generate an invoice for this order.')},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
elif has_inv:
|
||||
return Response(
|
||||
{'detail': _('An invoice for this order already exists.')},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
inv = generate_invoice(order)
|
||||
order.log_action(
|
||||
'pretix.event.order.invoice.generated',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'invoice': inv.pk
|
||||
}
|
||||
)
|
||||
return Response(
|
||||
InvoiceSerializer(inv).data,
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def resend_link(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
if not order.email:
|
||||
return Response({'detail': 'There is no email address associated with this order.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
try:
|
||||
order.resend_link(user=self.request.user, auth=self.request.auth)
|
||||
except SendMailException:
|
||||
return Response({'detail': _('There was an error sending the mail. Please try again later.')}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
|
||||
return Response(
|
||||
status=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@transaction.atomic
|
||||
def regenerate_secrets(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
order.secret = generate_secret()
|
||||
for op in order.all_positions.all():
|
||||
op.secret = generate_position_secret()
|
||||
op.save()
|
||||
order.save(update_fields=['secret'])
|
||||
CachedTicket.objects.filter(order_position__order=order).delete()
|
||||
CachedCombinedTicket.objects.filter(order=order).delete()
|
||||
order.log_action(
|
||||
'pretix.event.order.secret.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
def extend(self, request, **kwargs):
|
||||
new_date = request.data.get('expires', None)
|
||||
@@ -375,6 +441,71 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.get('partial', False)
|
||||
if not partial:
|
||||
return Response(
|
||||
{"detail": "Method \"PUT\" not allowed."},
|
||||
status=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
@transaction.atomic
|
||||
def perform_update(self, serializer):
|
||||
if 'comment' in self.request.data and serializer.instance.comment != self.request.data.get('comment'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.comment',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'new_comment': self.request.data.get('comment')
|
||||
}
|
||||
)
|
||||
|
||||
if 'checkin_attention' in self.request.data and serializer.instance.checkin_attention != self.request.data.get('checkin_attention'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.checkin_attention',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'new_value': self.request.data.get('checkin_attention')
|
||||
}
|
||||
)
|
||||
|
||||
if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.contact.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'old_email': serializer.instance.email,
|
||||
'new_email': self.request.data.get('email'),
|
||||
}
|
||||
)
|
||||
|
||||
if 'locale' in self.request.data and serializer.instance.locale != self.request.data.get('locale'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.locale.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'old_locale': serializer.instance.locale,
|
||||
'new_locale': self.request.data.get('locale'),
|
||||
}
|
||||
)
|
||||
|
||||
if 'invoice_address' in self.request.data:
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.modified',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'invoice_data': self.request.data.get('invoice_address'),
|
||||
}
|
||||
)
|
||||
|
||||
serializer.save()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
|
||||
@@ -397,6 +528,8 @@ class OrderPositionFilter(FilterSet):
|
||||
Q(secret__istartswith=value)
|
||||
| Q(attendee_name_cached__icontains=value)
|
||||
| Q(addon_to__attendee_name_cached__icontains=value)
|
||||
| Q(attendee_email__icontains=value)
|
||||
| Q(addon_to__attendee_email__icontains=value)
|
||||
| Q(order__code__istartswith=value)
|
||||
| Q(order__invoice_address__name_cached__icontains=value)
|
||||
| Q(order__email__icontains=value)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import logging
|
||||
from smtplib import SMTPResponseException
|
||||
|
||||
import bleach
|
||||
import markdown
|
||||
from django.conf import settings
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.dispatch import receiver
|
||||
@@ -12,7 +10,7 @@ from inlinestyler.utils import inline_css
|
||||
|
||||
from pretix.base.models import Event, Order
|
||||
from pretix.base.signals import register_html_mail_renderers
|
||||
from pretix.base.templatetags.rich_text import markdown_compile
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
|
||||
logger = logging.getLogger('pretix.base.email')
|
||||
|
||||
@@ -98,7 +96,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
raise NotImplementedError()
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order) -> str:
|
||||
body_md = bleach.linkify(markdown_compile(plain_body))
|
||||
body_md = markdown_compile_email(plain_body)
|
||||
htmlctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
@@ -112,7 +110,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
|
||||
if plain_signature:
|
||||
signature_md = plain_signature.replace('\n', '<br>\n')
|
||||
signature_md = bleach.linkify(bleach.clean(markdown.markdown(signature_md), tags=bleach.ALLOWED_TAGS + ['p', 'br']))
|
||||
signature_md = markdown_compile_email(signature_md)
|
||||
htmlctx['signature'] = signature_md
|
||||
|
||||
if order:
|
||||
|
||||
@@ -92,7 +92,7 @@ class ListExporter(BaseExporter):
|
||||
choices=(
|
||||
('xlsx', _('Excel (.xlsx)')),
|
||||
('default', _('CSV (with commas)')),
|
||||
('excel', _('CSV (Excel-style)')),
|
||||
('csv-excel', _('CSV (Excel-style)')),
|
||||
('semicolon', _('CSV (with semicolons)')),
|
||||
),
|
||||
)),
|
||||
|
||||
@@ -108,6 +108,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
]
|
||||
|
||||
headers.append(_('Invoice numbers'))
|
||||
headers.append(_('Sales channel'))
|
||||
|
||||
yield headers
|
||||
|
||||
@@ -176,6 +177,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
]
|
||||
|
||||
row.append(', '.join([i.number for i in order.invoices.all()]))
|
||||
row.append(order.sales_channel)
|
||||
yield row
|
||||
|
||||
def iterate_fees(self, form_data: dict):
|
||||
@@ -297,6 +299,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers += [
|
||||
_('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
|
||||
]
|
||||
headers.append(_('Sales channel'))
|
||||
|
||||
yield headers
|
||||
|
||||
@@ -351,6 +354,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
]
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
row += [''] * (7 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
|
||||
row.append(order.sales_channel)
|
||||
yield row
|
||||
|
||||
def get_filename(self):
|
||||
|
||||
@@ -9,6 +9,7 @@ import vat_moss.id
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
@@ -16,7 +17,7 @@ from pretix.base.forms.widgets import (
|
||||
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
|
||||
TimePickerWidget, UploadedFileWidget,
|
||||
)
|
||||
from pretix.base.models import InvoiceAddress, Question
|
||||
from pretix.base.models import InvoiceAddress, Question, QuestionOption
|
||||
from pretix.base.models.tax import EU_COUNTRIES
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
@@ -144,6 +145,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
item = pos.item
|
||||
questions = pos.item.questions_to_ask
|
||||
event = kwargs.pop('event')
|
||||
self.all_optional = kwargs.pop('all_optional', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -171,6 +173,9 @@ class BaseQuestionsForm(forms.Form):
|
||||
initial = None
|
||||
tz = pytz.timezone(event.settings.timezone)
|
||||
help_text = rich_text(q.help_text)
|
||||
default = q.default_value
|
||||
label = escape(q.question) # django-bootstrap3 calls mark_safe
|
||||
required = q.required and not self.all_optional
|
||||
if q.type == Question.TYPE_BOOLEAN:
|
||||
if q.required:
|
||||
# For some reason, django-bootstrap3 does not set the required attribute
|
||||
@@ -181,83 +186,99 @@ class BaseQuestionsForm(forms.Form):
|
||||
|
||||
if initial:
|
||||
initialbool = (initial.answer == "True")
|
||||
elif default:
|
||||
initialbool = (default == "True")
|
||||
else:
|
||||
initialbool = False
|
||||
|
||||
field = forms.BooleanField(
|
||||
label=q.question, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=initialbool, widget=widget,
|
||||
)
|
||||
elif q.type == Question.TYPE_NUMBER:
|
||||
field = forms.DecimalField(
|
||||
label=q.question, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=q.help_text,
|
||||
initial=initial.answer if initial else None,
|
||||
initial=initial.answer if initial else (default if default else None),
|
||||
min_value=Decimal('0.00'),
|
||||
)
|
||||
elif q.type == Question.TYPE_STRING:
|
||||
field = forms.CharField(
|
||||
label=q.question, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=initial.answer if initial else None,
|
||||
initial=initial.answer if initial else (default if default else None),
|
||||
)
|
||||
elif q.type == Question.TYPE_TEXT:
|
||||
field = forms.CharField(
|
||||
label=q.question, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
widget=forms.Textarea,
|
||||
initial=initial.answer if initial else None,
|
||||
initial=initial.answer if initial else (default if default else None),
|
||||
)
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
field = forms.ModelChoiceField(
|
||||
queryset=q.options,
|
||||
label=q.question, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
widget=forms.Select,
|
||||
to_field_name='identifier',
|
||||
empty_label='',
|
||||
initial=initial.options.first() if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
field = forms.ModelMultipleChoiceField(
|
||||
queryset=q.options,
|
||||
label=q.question, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
to_field_name='identifier',
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
initial=initial.options.all() if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_FILE:
|
||||
field = forms.FileField(
|
||||
label=q.question, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=initial.file if initial else None,
|
||||
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
|
||||
)
|
||||
elif q.type == Question.TYPE_DATE:
|
||||
field = forms.DateField(
|
||||
label=q.question, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None,
|
||||
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else (
|
||||
dateutil.parser.parse(default).date() if default else None),
|
||||
widget=DatePickerWidget(),
|
||||
)
|
||||
elif q.type == Question.TYPE_TIME:
|
||||
field = forms.TimeField(
|
||||
label=q.question, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else None,
|
||||
initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else (
|
||||
dateutil.parser.parse(default).time() if default else None),
|
||||
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
)
|
||||
elif q.type == Question.TYPE_DATETIME:
|
||||
field = SplitDateTimeField(
|
||||
label=q.question, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
|
||||
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else (
|
||||
dateutil.parser.parse(default).astimezone(tz) if default else None),
|
||||
widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
)
|
||||
field.question = q
|
||||
if answers:
|
||||
# Cache the answer object for later use
|
||||
field.answer = answers[0]
|
||||
|
||||
if q.dependency_question_id:
|
||||
field.widget.attrs['data-question-dependency'] = q.dependency_question_id
|
||||
field.widget.attrs['data-question-dependency-value'] = q.dependency_value
|
||||
if q.type != 'M':
|
||||
field.widget.attrs['required'] = q.required and not self.all_optional
|
||||
field._required = q.required and not self.all_optional
|
||||
field.required = False
|
||||
|
||||
self.fields['question_%s' % q.id] = field
|
||||
|
||||
responses = question_form_fields.send(sender=event, position=pos)
|
||||
@@ -268,6 +289,40 @@ class BaseQuestionsForm(forms.Form):
|
||||
self.fields[key] = value
|
||||
value.initial = data.get('question_form_data', {}).get(key)
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
|
||||
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
||||
|
||||
def question_is_visible(parentid, qval):
|
||||
parentq = question_cache[parentid]
|
||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_value):
|
||||
return False
|
||||
if 'question_%d' % parentid not in d:
|
||||
return False
|
||||
dval = d.get('question_%d' % parentid)
|
||||
if qval == 'True':
|
||||
return dval
|
||||
elif qval == 'False':
|
||||
return not dval
|
||||
elif isinstance(dval, QuestionOption):
|
||||
return dval.identifier == qval
|
||||
else:
|
||||
return qval in [o.identifier for o in dval]
|
||||
|
||||
def question_is_required(q):
|
||||
return (
|
||||
q.required and
|
||||
(not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_value))
|
||||
)
|
||||
|
||||
if not self.all_optional:
|
||||
for q in question_cache.values():
|
||||
if question_is_required(q) and not d.get('question_%d' % q.pk):
|
||||
raise ValidationError({'question_%d' % q.pk: [_('This field is required')]})
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
vat_warning = False
|
||||
|
||||
@@ -9,7 +9,7 @@ import vat_moss.exchange_rates
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.translation import pgettext, ugettext
|
||||
from django.utils.translation import get_language, pgettext, ugettext
|
||||
from PIL.Image import BICUBIC
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.enums import TA_LEFT
|
||||
@@ -138,6 +138,12 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
|
||||
italic='OpenSansIt', boldItalic='OpenSansBI')
|
||||
|
||||
def _upper(self, val):
|
||||
# We uppercase labels, but not in every language
|
||||
if get_language() == 'el':
|
||||
return val
|
||||
return val.upper()
|
||||
|
||||
def _on_other_page(self, canvas: Canvas, doc):
|
||||
"""
|
||||
Called when a new page is rendered that is *not* the first page.
|
||||
@@ -279,21 +285,21 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(pgettext('invoice', 'Invoice from').upper())
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice from')))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
self._draw_invoice_from(canvas)
|
||||
|
||||
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(pgettext('invoice', 'Invoice to').upper())
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice to')))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
self._draw_invoice_to(canvas)
|
||||
|
||||
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(pgettext('invoice', 'Order code').upper())
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Order code')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self.invoice.order.full_code)
|
||||
@@ -302,18 +308,18 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
if self.invoice.is_cancellation:
|
||||
textobject.textLine(pgettext('invoice', 'Cancellation number').upper())
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation number')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self.invoice.number)
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(pgettext('invoice', 'Original invoice').upper())
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self.invoice.refers.number)
|
||||
else:
|
||||
textobject.textLine(pgettext('invoice', 'Invoice number').upper())
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice number')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self.invoice.number)
|
||||
@@ -321,20 +327,20 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if self.invoice.is_cancellation:
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(pgettext('invoice', 'Cancellation date').upper())
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation date')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(pgettext('invoice', 'Original invoice date').upper())
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice date')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(date_format(self.invoice.refers.date, "DATE_FORMAT"))
|
||||
textobject.moveCursor(0, 5)
|
||||
else:
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(pgettext('invoice', 'Invoice date').upper())
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice date')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
|
||||
@@ -388,7 +394,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(pgettext('invoice', 'Event').upper())
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Event')))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
canvas.restoreState()
|
||||
|
||||
27
src/pretix/base/migrations/0112_auto_20190304_1726.py
Normal file
27
src/pretix/base/migrations/0112_auto_20190304_1726.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 2.1.5 on 2019-03-04 17:26
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.models import Count
|
||||
|
||||
|
||||
def make_checkins_unique(apps, se):
|
||||
Checkin = apps.get_model('pretixbase', 'Checkin')
|
||||
for d in Checkin.objects.order_by().values('list_id', 'position_id').annotate(c=Count('id')).filter(c__gt=1):
|
||||
for c in Checkin.objects.filter(list_id=d['list_id'], position_id=d['position_id'])[:d['c'] - 1]:
|
||||
c.delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0111_auto_20190219_0949'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
make_checkins_unique, migrations.RunPython.noop,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='checkin',
|
||||
unique_together={('list', 'position')},
|
||||
),
|
||||
]
|
||||
27
src/pretix/base/migrations/0113_auto_20190312_0942.py
Normal file
27
src/pretix/base/migrations/0113_auto_20190312_0942.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 2.1.5 on 2019-03-12 09:42
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0112_auto_20190304_1726'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='dependency_question',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dependent_questions', to='pretixbase.Question'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='dependency_value',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
60
src/pretix/base/migrations/0114_auto_20190316_1014.py
Normal file
60
src/pretix/base/migrations/0114_auto_20190316_1014.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-16 10:14
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0113_auto_20190312_0942'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ItemBundle',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('count', models.PositiveIntegerField(default=1, verbose_name='Number')),
|
||||
('designated_price', models.DecimalField(blank=True, decimal_places=2, help_text="If set, it will be shown that this bundled item is responsible for the given value of the total price. This might be important in cases of mixed taxation, but can be kept blank otherwise. This value will NOT be added to the base item's price.", max_digits=10, null=True, verbose_name='Designated price part')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='is_bundled',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='addon_to',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='addons', to='pretixbase.CartPosition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='addon_to',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='addons', to='pretixbase.OrderPosition'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itembundle',
|
||||
name='base_item',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bundles', to='pretixbase.Item'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itembundle',
|
||||
name='bundled_item',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bundled_with', to='pretixbase.Item', verbose_name='Bundled item'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itembundle',
|
||||
name='bundled_variation',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bundled_with', to='pretixbase.ItemVariation', verbose_name='Bundled variation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='require_bundling',
|
||||
field=models.BooleanField(default=False, help_text='If this option is set, the product will only be sold as part of bundle products.', verbose_name='Only sell this product as part of a bundle'),
|
||||
),
|
||||
]
|
||||
24
src/pretix/base/migrations/0115_auto_20190323_2238.py
Normal file
24
src/pretix/base/migrations/0115_auto_20190323_2238.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-23 22:38
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0114_auto_20190316_1014'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='itembundle',
|
||||
name='designated_price',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, default=Decimal('0.00'), help_text="If set, it will be shown that this bundled item is responsible for the given value of the total gross price. This might be important in cases of mixed taxation, but can be kept blank otherwise. This value will NOT be added to the base item's price.", max_digits=10, verbose_name='Designated price part'),
|
||||
),
|
||||
]
|
||||
20
src/pretix/base/migrations/0116_auto_20180531_1739.py
Normal file
20
src/pretix/base/migrations/0116_auto_20180531_1739.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-05-31 15:39
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0115_auto_20190323_2238'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='default_value',
|
||||
field=models.TextField(blank=True, help_text='The question will be filled with this response by default', null=True, verbose_name='Default answer'),
|
||||
),
|
||||
]
|
||||
@@ -9,8 +9,9 @@ from .event import (
|
||||
)
|
||||
from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||
from .items import (
|
||||
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
|
||||
Quota, SubEventItem, SubEventItemVariation, itempicture_upload_to,
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
|
||||
QuestionOption, Quota, SubEventItem, SubEventItemVariation,
|
||||
itempicture_upload_to,
|
||||
)
|
||||
from .log import LogEntry
|
||||
from .notifications import NotificationSetting
|
||||
|
||||
@@ -167,6 +167,9 @@ class Checkin(models.Model):
|
||||
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('list', 'position'),)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Checkin: pos {} on list '{}' at {}>".format(
|
||||
self.position, self.list, self.datetime
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import string
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, time
|
||||
from datetime import datetime, time, timedelta
|
||||
from operator import attrgetter
|
||||
|
||||
import pytz
|
||||
@@ -515,6 +515,10 @@ class Event(EventMixin, LoggedModel):
|
||||
o.question = q
|
||||
o.save()
|
||||
|
||||
for q in self.questions.filter(dependency_question__isnull=False):
|
||||
q.dependency_question = question_map[q.dependency_question_id]
|
||||
q.save(update_fields=['dependency_question'])
|
||||
|
||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
|
||||
items = list(cl.limit_products.all())
|
||||
cl.pk = None
|
||||
@@ -664,8 +668,8 @@ class Event(EventMixin, LoggedModel):
|
||||
}[ordering]
|
||||
subevs = queryset.filter(
|
||||
Q(active=True) & (
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||
| Q(date_to__gte=now())
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24)))
|
||||
| Q(date_to__gte=now() - timedelta(hours=24))
|
||||
)
|
||||
) # order_by doesn't make sense with I18nField
|
||||
for f in reversed(orderfields):
|
||||
@@ -932,6 +936,18 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
if self.event:
|
||||
self.event.cache.clear()
|
||||
|
||||
@staticmethod
|
||||
def clean_items(event, items):
|
||||
for item in items:
|
||||
if event != item.event:
|
||||
raise ValidationError(_('One or more items do not belong to this event.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_variations(event, variations):
|
||||
for variation in variations:
|
||||
if event != variation.item.event:
|
||||
raise ValidationError(_('One or more variations do not belong to this event.'))
|
||||
|
||||
|
||||
def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import sys
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from datetime import date, datetime, time
|
||||
from decimal import Decimal, DecimalException
|
||||
from typing import Tuple
|
||||
@@ -161,7 +162,7 @@ class ItemQuerySet(models.QuerySet):
|
||||
Q(active=True)
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
||||
& Q(sales_channels__contains=channel)
|
||||
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
||||
)
|
||||
if not allow_addons:
|
||||
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
||||
@@ -328,6 +329,11 @@ class Item(LoggedModel):
|
||||
help_text=_('This product will be hidden from the event page until the user enters a voucher '
|
||||
'code that is specifically tied to this product (and not via a quota).')
|
||||
)
|
||||
require_bundling = models.BooleanField(
|
||||
verbose_name=_('Only sell this product as part of a bundle'),
|
||||
default=False,
|
||||
help_text=_('If this option is set, the product will only be sold as part of bundle products.')
|
||||
)
|
||||
allow_cancel = models.BooleanField(
|
||||
verbose_name=_('Allow product to be canceled'),
|
||||
default=True,
|
||||
@@ -386,12 +392,28 @@ class Item(LoggedModel):
|
||||
if self.event:
|
||||
self.event.cache.clear()
|
||||
|
||||
def tax(self, price=None, base_price_is='auto'):
|
||||
def tax(self, price=None, base_price_is='auto', currency=None, include_bundled=False):
|
||||
price = price if price is not None else self.default_price
|
||||
|
||||
if not self.tax_rule:
|
||||
return TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
||||
rate=Decimal('0.00'), name='')
|
||||
return self.tax_rule.tax(price, base_price_is=base_price_is)
|
||||
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
||||
rate=Decimal('0.00'), name='')
|
||||
else:
|
||||
t = self.tax_rule.tax(price, base_price_is=base_price_is, currency=currency)
|
||||
|
||||
if include_bundled:
|
||||
for b in self.bundles.all():
|
||||
if b.designated_price and b.bundled_item.tax_rule_id != self.tax_rule_id:
|
||||
if b.bundled_variation:
|
||||
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
|
||||
else:
|
||||
bprice = b.bundled_item.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
|
||||
compare_price = self.tax_rule.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
|
||||
t.net += bprice.net - compare_price.net
|
||||
t.tax += bprice.tax - compare_price.tax
|
||||
t.name = "MIXED!"
|
||||
|
||||
return t
|
||||
|
||||
def is_available_by_time(self, now_dt: datetime=None) -> bool:
|
||||
now_dt = now_dt or now()
|
||||
@@ -411,7 +433,18 @@ class Item(LoggedModel):
|
||||
return False
|
||||
return True
|
||||
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None):
|
||||
def _get_quotas(self, ignored_quotas=None, subevent=None):
|
||||
check_quotas = set(getattr(
|
||||
self, '_subevent_quotas', # Utilize cache in product list
|
||||
self.quotas.filter(subevent=subevent).select_related('subevent')
|
||||
if subevent else self.quotas.all()
|
||||
))
|
||||
if ignored_quotas:
|
||||
check_quotas -= set(ignored_quotas)
|
||||
return check_quotas
|
||||
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None,
|
||||
include_bundled=False, trust_parameters=False):
|
||||
"""
|
||||
This method is used to determine whether this Item is currently available
|
||||
for sale.
|
||||
@@ -420,33 +453,60 @@ class Item(LoggedModel):
|
||||
quotas will be ignored in the calculation. If this leads
|
||||
to no quotas being checked at all, this method will return
|
||||
unlimited availability.
|
||||
:param include_bundled: Also take availability of bundled items into consideration.
|
||||
:param trust_parameters: Disable checking of the subevent parameter and disable checking if
|
||||
any variations exist (performance optimization).
|
||||
:returns: any of the return codes of :py:meth:`Quota.availability()`.
|
||||
|
||||
:raises ValueError: if you call this on an item which has variations associated with it.
|
||||
Please use the method on the ItemVariation object you are interested in.
|
||||
"""
|
||||
check_quotas = set(getattr(
|
||||
self, '_subevent_quotas', # Utilize cache in product list
|
||||
self.quotas.select_related('subevent').filter(subevent=subevent)
|
||||
if subevent else self.quotas.all()
|
||||
))
|
||||
if not subevent and self.event.has_subevents:
|
||||
if not trust_parameters and not subevent and self.event.has_subevents:
|
||||
raise TypeError('You need to supply a subevent.')
|
||||
if ignored_quotas:
|
||||
check_quotas -= set(ignored_quotas)
|
||||
if not check_quotas:
|
||||
return Quota.AVAILABILITY_OK, sys.maxsize
|
||||
if self.has_variations: # NOQA
|
||||
raise ValueError('Do not call this directly on items which have variations '
|
||||
'but call this on their ItemVariation objects')
|
||||
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
|
||||
key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize))
|
||||
check_quotas = self._get_quotas(ignored_quotas=ignored_quotas, subevent=subevent)
|
||||
quotacounter = Counter()
|
||||
res = Quota.AVAILABILITY_OK, None
|
||||
for q in check_quotas:
|
||||
quotacounter[q] += 1
|
||||
|
||||
if include_bundled:
|
||||
for b in self.bundles.all():
|
||||
bundled_check_quotas = (b.bundled_variation or b.bundled_item)._get_quotas(ignored_quotas=ignored_quotas, subevent=subevent)
|
||||
if not bundled_check_quotas:
|
||||
return Quota.AVAILABILITY_GONE, 0
|
||||
for q in bundled_check_quotas:
|
||||
quotacounter[q] += b.count
|
||||
|
||||
for q, n in quotacounter.items():
|
||||
a = q.availability(count_waitinglist=count_waitinglist, _cache=_cache)
|
||||
if a[1] is None:
|
||||
continue
|
||||
|
||||
num_avail = a[1] // n
|
||||
code_avail = Quota.AVAILABILITY_GONE if a[1] >= 1 and num_avail < 1 else a[0]
|
||||
# this is not entirely accurate, as it shows "sold out" even if it is actually just "reserved",
|
||||
# since we do not know that distinction here if at least one item is available. However, this
|
||||
# is only relevant in connection with bundles.
|
||||
|
||||
if code_avail < res[0] or res[1] is None or num_avail < res[1]:
|
||||
res = (code_avail, num_avail)
|
||||
|
||||
if len(quotacounter) == 0:
|
||||
return Quota.AVAILABILITY_OK, sys.maxsize # backwards compatibility
|
||||
return res
|
||||
|
||||
def allow_delete(self):
|
||||
from pretix.base.models.orders import OrderPosition
|
||||
|
||||
return not OrderPosition.all.filter(item=self).exists()
|
||||
|
||||
@property
|
||||
def includes_mixed_tax_rate(self):
|
||||
for b in self.bundles.all():
|
||||
if b.designated_price and b.bundled_item.tax_rule_id != self.tax_rule_id:
|
||||
return True
|
||||
return False
|
||||
|
||||
@cached_property
|
||||
def has_variations(self):
|
||||
return self.variations.exists()
|
||||
@@ -531,11 +591,28 @@ class ItemVariation(models.Model):
|
||||
def price(self):
|
||||
return self.default_price if self.default_price is not None else self.item.default_price
|
||||
|
||||
def tax(self, price=None):
|
||||
def tax(self, price=None, base_price_is='auto', currency=None, include_bundled=False):
|
||||
price = price if price is not None else self.price
|
||||
|
||||
if not self.item.tax_rule:
|
||||
return TaxedPrice(gross=price, net=price, tax=Decimal('0.00'), rate=Decimal('0.00'), name='')
|
||||
return self.item.tax_rule.tax(price)
|
||||
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
||||
rate=Decimal('0.00'), name='')
|
||||
else:
|
||||
t = self.item.tax_rule.tax(price, base_price_is=base_price_is, currency=currency)
|
||||
|
||||
if include_bundled:
|
||||
for b in self.item.bundles.all():
|
||||
if b.designated_price and b.bundled_item.tax_rule_id != self.item.tax_rule_id:
|
||||
if b.bundled_variation:
|
||||
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
|
||||
else:
|
||||
bprice = b.bundled_item.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
|
||||
compare_price = self.item.tax_rule.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
|
||||
t.net += bprice.net - compare_price.net
|
||||
t.tax += bprice.tax - compare_price.tax
|
||||
t.name = "MIXED!"
|
||||
|
||||
return t
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
@@ -547,7 +624,18 @@ class ItemVariation(models.Model):
|
||||
if self.item:
|
||||
self.item.event.cache.clear()
|
||||
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None) -> Tuple[int, int]:
|
||||
def _get_quotas(self, ignored_quotas=None, subevent=None):
|
||||
check_quotas = set(getattr(
|
||||
self, '_subevent_quotas', # Utilize cache in product list
|
||||
self.quotas.filter(subevent=subevent).select_related('subevent')
|
||||
if subevent else self.quotas.all()
|
||||
))
|
||||
if ignored_quotas:
|
||||
check_quotas -= set(ignored_quotas)
|
||||
return check_quotas
|
||||
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None,
|
||||
include_bundled=False, trust_parameters=False) -> Tuple[int, int]:
|
||||
"""
|
||||
This method is used to determine whether this ItemVariation is currently
|
||||
available for sale in terms of quotas.
|
||||
@@ -559,19 +647,38 @@ class ItemVariation(models.Model):
|
||||
:param count_waitinglist: If ``False``, waiting list entries will be ignored for quota calculation.
|
||||
:returns: any of the return codes of :py:meth:`Quota.availability()`.
|
||||
"""
|
||||
check_quotas = set(getattr(
|
||||
self, '_subevent_quotas', # Utilize cache in product list
|
||||
self.quotas.filter(subevent=subevent).select_related('subevent')
|
||||
if subevent else self.quotas.all()
|
||||
))
|
||||
if ignored_quotas:
|
||||
check_quotas -= set(ignored_quotas)
|
||||
if not subevent and self.item.event.has_subevents: # NOQA
|
||||
if not trust_parameters and not subevent and self.item.event.has_subevents: # NOQA
|
||||
raise TypeError('You need to supply a subevent.')
|
||||
if not check_quotas:
|
||||
return Quota.AVAILABILITY_OK, sys.maxsize
|
||||
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
|
||||
key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize))
|
||||
check_quotas = self._get_quotas(ignored_quotas=ignored_quotas, subevent=subevent)
|
||||
quotacounter = Counter()
|
||||
res = Quota.AVAILABILITY_OK, None
|
||||
for q in check_quotas:
|
||||
quotacounter[q] += 1
|
||||
|
||||
if include_bundled:
|
||||
for b in self.item.bundles.all():
|
||||
bundled_check_quotas = (b.bundled_variation or b.bundled_item)._get_quotas(ignored_quotas=ignored_quotas, subevent=subevent)
|
||||
if not bundled_check_quotas:
|
||||
return Quota.AVAILABILITY_GONE, 0
|
||||
for q in bundled_check_quotas:
|
||||
quotacounter[q] += b.count
|
||||
|
||||
for q, n in quotacounter.items():
|
||||
a = q.availability(count_waitinglist=count_waitinglist, _cache=_cache)
|
||||
if a[1] is None:
|
||||
continue
|
||||
|
||||
num_avail = a[1] // n
|
||||
code_avail = Quota.AVAILABILITY_GONE if a[1] >= 1 and num_avail < 1 else a[0]
|
||||
# this is not entirely accurate, as it shows "sold out" even if it is actually just "reserved",
|
||||
# since we do not know that distinction here if at least one item is available. However, this
|
||||
# is only relevant in connection with bundles.
|
||||
|
||||
if code_avail < res[0] or res[1] is None or num_avail < res[1]:
|
||||
res = (code_avail, num_avail)
|
||||
if len(quotacounter) == 0:
|
||||
return Quota.AVAILABILITY_OK, sys.maxsize # backwards compatibility
|
||||
return res
|
||||
|
||||
def __lt__(self, other):
|
||||
if self.position == other.position:
|
||||
@@ -672,6 +779,83 @@ class ItemAddOn(models.Model):
|
||||
raise ValidationError(_('The maximum count needs to be greater than the minimum count.'))
|
||||
|
||||
|
||||
class ItemBundle(models.Model):
|
||||
"""
|
||||
An instance of this model indicates that buying a ticket of the type ``base_item``
|
||||
automatically also buys ``count`` items of type ``bundled_item``.
|
||||
|
||||
:param base_item: The base item the bundle is attached to
|
||||
:type base_item: Item
|
||||
:param bundled_item: The bundled item
|
||||
:type bundled_item: Item
|
||||
:param bundled_variation: The variation, if the bundled item has variations
|
||||
:type bundled_variation: ItemVariation
|
||||
:param count: The number of items to bundle
|
||||
:type count: int
|
||||
:param designated_price: The designated part price (optional)
|
||||
:type designated_price: bool
|
||||
"""
|
||||
base_item = models.ForeignKey(
|
||||
Item,
|
||||
related_name='bundles',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
bundled_item = models.ForeignKey(
|
||||
Item,
|
||||
related_name='bundled_with',
|
||||
verbose_name=_('Bundled item'),
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
bundled_variation = models.ForeignKey(
|
||||
ItemVariation,
|
||||
related_name='bundled_with',
|
||||
verbose_name=_('Bundled variation'),
|
||||
null=True, blank=True,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
count = models.PositiveIntegerField(
|
||||
default=1,
|
||||
verbose_name=_('Number')
|
||||
)
|
||||
designated_price = models.DecimalField(
|
||||
default=Decimal('0.00'), blank=True,
|
||||
decimal_places=2, max_digits=10,
|
||||
verbose_name=_('Designated price part'),
|
||||
help_text=_('If set, it will be shown that this bundled item is responsible for the given value of the total '
|
||||
'gross price. This might be important in cases of mixed taxation, but can be kept blank otherwise. This '
|
||||
'value will NOT be added to the base item\'s price.')
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
self.clean_count(self.count)
|
||||
|
||||
def describe(self):
|
||||
if self.count == 1:
|
||||
if self.bundled_variation_id:
|
||||
return "{} – {}".format(self.bundled_item.name, self.bundled_variation.value)
|
||||
else:
|
||||
return self.bundled_item.name
|
||||
else:
|
||||
if self.bundled_variation_id:
|
||||
return "{}× {} – {}".format(self.count, self.bundled_item.name, self.bundled_variation.value)
|
||||
else:
|
||||
return "{}x {}".format(self.count, self.bundled_item.name)
|
||||
|
||||
@staticmethod
|
||||
def clean_itemvar(event, bundled_item, bundled_variation):
|
||||
if event != bundled_item.event:
|
||||
raise ValidationError(_('The bundled item must belong to the same event as the item.'))
|
||||
if bundled_item.has_variations and not bundled_variation:
|
||||
raise ValidationError(_('A variation needs to be set for this item.'))
|
||||
if bundled_variation and bundled_variation.item != bundled_item:
|
||||
raise ValidationError(_('The chosen variation does not belong to this item.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_count(count):
|
||||
if count < 0:
|
||||
raise ValidationError(_('The count needs to be equal to or greater than zero.'))
|
||||
|
||||
|
||||
class Question(LoggedModel):
|
||||
"""
|
||||
A question is an input field that can be used to extend a ticket by custom information,
|
||||
@@ -702,6 +886,10 @@ class Question(LoggedModel):
|
||||
:type ask_during_checkin: bool
|
||||
:param identifier: An arbitrary, internal identifier
|
||||
:type identifier: str
|
||||
:param dependency_question: This question will only show up if the referenced question is set to `dependency_value`.
|
||||
:type dependency_question: Question
|
||||
:param dependency_value: The value that `dependency_question` needs to be set to for this question to be applicable.
|
||||
:type dependency_value: str
|
||||
"""
|
||||
TYPE_NUMBER = "N"
|
||||
TYPE_STRING = "S"
|
||||
@@ -771,6 +959,15 @@ class Question(LoggedModel):
|
||||
'pretixdesk 0.2 or newer.'),
|
||||
default=False
|
||||
)
|
||||
dependency_question = models.ForeignKey(
|
||||
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
||||
)
|
||||
dependency_value = models.TextField(null=True, blank=True)
|
||||
default_value = models.TextField(
|
||||
verbose_name=_('Default answer'),
|
||||
help_text=_('The question will be filled with this response by default'),
|
||||
null=True, blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Question")
|
||||
@@ -788,6 +985,10 @@ class Question(LoggedModel):
|
||||
def clean_identifier(self, code):
|
||||
Question._clean_identifier(self.event, code, self)
|
||||
|
||||
def clean(self):
|
||||
if self.default_value is not None:
|
||||
self.clean_answer(self.default_value, check_required=False)
|
||||
|
||||
@staticmethod
|
||||
def _clean_identifier(event, code, instance=None):
|
||||
qs = Question.objects.filter(event=event, identifier__iexact=code)
|
||||
@@ -815,8 +1016,8 @@ class Question(LoggedModel):
|
||||
def __lt__(self, other) -> bool:
|
||||
return self.sortkey < other.sortkey
|
||||
|
||||
def clean_answer(self, answer):
|
||||
if self.required:
|
||||
def clean_answer(self, answer, check_required=True):
|
||||
if self.required and check_required:
|
||||
if not answer or (self.type == Question.TYPE_BOOLEAN and answer not in ("true", "True", True)):
|
||||
raise ValidationError(_('An answer to this question is required to proceed.'))
|
||||
if not answer:
|
||||
|
||||
@@ -229,6 +229,8 @@ class Order(LockModel, LoggedModel):
|
||||
|
||||
@cached_property
|
||||
def meta_info_data(self):
|
||||
if not self.meta_info:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(self.meta_info)
|
||||
except TypeError:
|
||||
@@ -713,6 +715,33 @@ class Order(LockModel, LoggedModel):
|
||||
}
|
||||
)
|
||||
|
||||
def resend_link(self, user=None, auth=None):
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
with language(self.locale):
|
||||
try:
|
||||
invoice_name = self.invoice_address.name
|
||||
invoice_company = self.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
email_template = self.event.settings.mail_text_resend_link
|
||||
email_context = {
|
||||
'event': self.event.name,
|
||||
'url': build_absolute_uri(self.event, 'presale:event.order', kwargs={
|
||||
'order': self.code,
|
||||
'secret': self.secret
|
||||
}),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
}
|
||||
email_subject = _('Your order: %(code)s') % {'code': self.code}
|
||||
self.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.resend', user=user, auth=auth,
|
||||
attach_tickets=True
|
||||
)
|
||||
|
||||
@property
|
||||
def positions_with_tickets(self):
|
||||
for op in self.positions.all():
|
||||
@@ -944,16 +973,38 @@ class AbstractPosition(models.Model):
|
||||
# selected via prefetch_related
|
||||
if not all:
|
||||
if hasattr(self.item, 'questions_to_ask'):
|
||||
self.questions = list(copy.copy(q) for q in self.item.questions_to_ask)
|
||||
questions = list(copy.copy(q) for q in self.item.questions_to_ask)
|
||||
else:
|
||||
self.questions = list(copy.copy(q) for q in self.item.questions.filter(ask_during_checkin=False))
|
||||
questions = list(copy.copy(q) for q in self.item.questions.filter(ask_during_checkin=False))
|
||||
else:
|
||||
self.questions = list(copy.copy(q) for q in self.item.questions.all())
|
||||
for q in self.questions:
|
||||
questions = list(copy.copy(q) for q in self.item.questions.all())
|
||||
|
||||
question_cache = {
|
||||
q.pk: q for q in questions
|
||||
}
|
||||
|
||||
def question_is_visible(parentid, qval):
|
||||
parentq = question_cache[parentid]
|
||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_value):
|
||||
return False
|
||||
if parentid not in self.answ:
|
||||
return False
|
||||
if qval == 'True':
|
||||
return self.answ[parentid].answer == 'True'
|
||||
elif qval == 'False':
|
||||
return self.answ[parentid].answer == 'False'
|
||||
else:
|
||||
return qval in [o.identifier for o in self.answ[parentid].options.all()]
|
||||
|
||||
self.questions = []
|
||||
for q in questions:
|
||||
if q.id in self.answ:
|
||||
q.answer = self.answ[q.id]
|
||||
q.answer.question = q # cache object
|
||||
else:
|
||||
q.answer = ""
|
||||
if not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_value):
|
||||
self.questions.append(q)
|
||||
|
||||
@property
|
||||
def net_price(self):
|
||||
@@ -1086,7 +1137,7 @@ class OrderPayment(models.Model):
|
||||
"""
|
||||
return self.order.event.get_payment_providers().get(self.provider)
|
||||
|
||||
def _mark_paid(self, force, count_waitinglist, user, auth):
|
||||
def _mark_paid(self, force, count_waitinglist, user, auth, overpaid=False):
|
||||
from pretix.base.signals import order_paid
|
||||
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist)
|
||||
if not force and can_be_paid is not True:
|
||||
@@ -1103,6 +1154,9 @@ class OrderPayment(models.Model):
|
||||
'date': self.payment_date,
|
||||
'force': force
|
||||
}, user=user, auth=auth)
|
||||
|
||||
if overpaid:
|
||||
self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth)
|
||||
order_paid.send(self.order.event, order=self.order)
|
||||
|
||||
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text=''):
|
||||
@@ -1168,10 +1222,10 @@ class OrderPayment(models.Model):
|
||||
# Performance optimization. In this case, there's really no reason to lock everything and an atomic
|
||||
# database transaction is more than enough.
|
||||
with transaction.atomic():
|
||||
self._mark_paid(force, count_waitinglist, user, auth)
|
||||
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total)
|
||||
else:
|
||||
with self.order.event.lock():
|
||||
self._mark_paid(force, count_waitinglist, user, auth)
|
||||
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total)
|
||||
|
||||
invoice = None
|
||||
if invoice_qualified(self.order):
|
||||
@@ -1664,8 +1718,9 @@ class OrderPosition(AbstractPosition):
|
||||
# Delete afterwards. Deleting in between might cause deletion of things related to add-ons
|
||||
# due to the deletion cascade.
|
||||
for cartpos in cp:
|
||||
cartpos.addons.all().delete()
|
||||
cartpos.delete()
|
||||
if cartpos.pk:
|
||||
cartpos.addons.all().delete()
|
||||
cartpos.delete()
|
||||
return ops
|
||||
|
||||
def __str__(self):
|
||||
@@ -1762,6 +1817,7 @@ class CartPosition(AbstractPosition):
|
||||
includes_tax = models.BooleanField(
|
||||
default=True
|
||||
)
|
||||
is_bundled = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Cart position")
|
||||
|
||||
@@ -33,6 +33,32 @@ class TaxedPrice:
|
||||
money_filter(self.gross, currency)
|
||||
)
|
||||
|
||||
def __sub__(self, other):
|
||||
newgross = self.gross - other.gross
|
||||
newnet = round_decimal(newgross - (newgross * (1 - 100 / (100 + self.rate)))).quantize(
|
||||
Decimal('10') ** self.gross.as_tuple().exponent
|
||||
)
|
||||
return TaxedPrice(
|
||||
gross=newgross,
|
||||
net=newnet,
|
||||
tax=newgross - newnet,
|
||||
rate=self.rate,
|
||||
name=self.name,
|
||||
)
|
||||
|
||||
def __mul__(self, other):
|
||||
newgross = self.gross * other
|
||||
newnet = round_decimal(newgross - (newgross * (1 - 100 / (100 + self.rate)))).quantize(
|
||||
Decimal('10') ** self.gross.as_tuple().exponent
|
||||
)
|
||||
return TaxedPrice(
|
||||
gross=newgross,
|
||||
net=newnet,
|
||||
tax=newgross - newnet,
|
||||
rate=self.rate,
|
||||
name=self.name,
|
||||
)
|
||||
|
||||
|
||||
TAXED_ZERO = TaxedPrice(
|
||||
gross=Decimal('0.00'),
|
||||
@@ -129,7 +155,12 @@ class TaxRule(LoggedModel):
|
||||
def has_custom_rules(self):
|
||||
return self.custom_rules and self.custom_rules != '[]'
|
||||
|
||||
def tax(self, base_price, base_price_is='auto'):
|
||||
def tax(self, base_price, base_price_is='auto', currency=None):
|
||||
from .event import Event
|
||||
try:
|
||||
currency = currency or self.event.currency
|
||||
except Event.DoesNotExist:
|
||||
pass
|
||||
if self.rate == Decimal('0.00'):
|
||||
return TaxedPrice(
|
||||
net=base_price, gross=base_price, tax=Decimal('0.00'),
|
||||
@@ -145,11 +176,11 @@ class TaxRule(LoggedModel):
|
||||
if base_price_is == 'gross':
|
||||
gross = base_price
|
||||
net = round_decimal(gross - (base_price * (1 - 100 / (100 + self.rate))),
|
||||
self.event.currency if self.event else None)
|
||||
currency)
|
||||
elif base_price_is == 'net':
|
||||
net = base_price
|
||||
gross = round_decimal((net * (1 + self.rate / 100)),
|
||||
self.event.currency if self.event else None)
|
||||
currency)
|
||||
else:
|
||||
raise ValueError('Unknown base price type: {}'.format(base_price_is))
|
||||
|
||||
|
||||
@@ -236,6 +236,12 @@ def register_default_notification_types(sender, **kwargs):
|
||||
_('Order changed'),
|
||||
_('Order {order.code} has been changed.')
|
||||
),
|
||||
ParametrizedOrderNotificationType(
|
||||
sender,
|
||||
'pretix.event.order.overpaid',
|
||||
_('Order has been overpaid'),
|
||||
_('Order {order.code} has been overpaid.')
|
||||
),
|
||||
ParametrizedOrderNotificationType(
|
||||
sender,
|
||||
'pretix.event.order.refund.created.externally',
|
||||
|
||||
@@ -311,7 +311,7 @@ class BasePaymentProvider:
|
||||
@property
|
||||
def payment_form_fields(self) -> dict:
|
||||
"""
|
||||
This is used by the default implementation of :py:meth:`checkout_form`.
|
||||
This is used by the default implementation of :py:meth:`payment_form`.
|
||||
It should return an object similar to :py:attr:`settings_form_fields`.
|
||||
|
||||
The default implementation returns an empty dictionary.
|
||||
@@ -320,10 +320,10 @@ class BasePaymentProvider:
|
||||
|
||||
def payment_form(self, request: HttpRequest) -> Form:
|
||||
"""
|
||||
This is called by the default implementation of :py:meth:`checkout_form_render`
|
||||
This is called by the default implementation of :py:meth:`payment_form_render`
|
||||
to obtain the form that is displayed to the user during the checkout
|
||||
process. The default implementation constructs the form using
|
||||
:py:attr:`checkout_form_fields` and sets appropriate prefixes for the form
|
||||
:py:attr:`payment_form_fields` and sets appropriate prefixes for the form
|
||||
and all fields and fills the form with data form the user's session.
|
||||
|
||||
If you overwrite this, we strongly suggest that you inherit from
|
||||
@@ -437,7 +437,7 @@ class BasePaymentProvider:
|
||||
When the user selects this provider as their preferred payment method,
|
||||
they will be shown the HTML you return from this method.
|
||||
|
||||
The default implementation will call :py:meth:`checkout_form`
|
||||
The default implementation will call :py:meth:`payment_form`
|
||||
and render the returned form. If your payment method doesn't require
|
||||
the user to fill out form fields, you should just return a paragraph
|
||||
of explanatory text.
|
||||
@@ -720,6 +720,29 @@ class BoxOfficeProvider(BasePaymentProvider):
|
||||
def order_change_allowed(self, order: Order) -> bool:
|
||||
return False
|
||||
|
||||
def payment_control_render(self, request, payment) -> str:
|
||||
template = None
|
||||
payment_info = None
|
||||
|
||||
if payment.info:
|
||||
payment_info = json.loads(payment.info)
|
||||
if payment_info['payment_type'] == "sumup":
|
||||
template = get_template('pretixcontrol/boxoffice/payment_sumup.html')
|
||||
|
||||
ctx = {
|
||||
'request': request,
|
||||
'event': self.event,
|
||||
'settings': self.settings,
|
||||
'payment_info': payment_info,
|
||||
'payment': payment,
|
||||
'provider': self,
|
||||
}
|
||||
|
||||
if template:
|
||||
return template.render(ctx)
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
class ManualPayment(BasePaymentProvider):
|
||||
identifier = 'manual'
|
||||
|
||||
@@ -110,7 +110,18 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
("event_begin", {
|
||||
"label": _("Event begin date and time"),
|
||||
"editor_sample": _("2017-05-31 20:00"),
|
||||
"evaluate": lambda op, order, ev: ev.get_date_from_display(show_times=True)
|
||||
"evaluate": lambda op, order, ev: date_format(
|
||||
ev.date_from.astimezone(timezone(ev.settings.timezone)),
|
||||
"SHORT_DATETIME_FORMAT"
|
||||
) if ev.date_from else ""
|
||||
}),
|
||||
("event_begin_date", {
|
||||
"label": _("Event begin date"),
|
||||
"editor_sample": _("2017-05-31"),
|
||||
"evaluate": lambda op, order, ev: date_format(
|
||||
ev.date_from.astimezone(timezone(ev.settings.timezone)),
|
||||
"SHORT_DATE_FORMAT"
|
||||
) if ev.date_from else ""
|
||||
}),
|
||||
("event_begin_time", {
|
||||
"label": _("Event begin time"),
|
||||
@@ -181,6 +192,7 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
|
||||
else op.addons.select_related('item', 'variation')
|
||||
)
|
||||
if not p.canceled
|
||||
])
|
||||
}),
|
||||
("organizer", {
|
||||
|
||||
@@ -13,7 +13,8 @@ from django.utils.translation import pgettext_lazy, ugettext as _
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Voucher,
|
||||
CartPosition, Event, InvoiceAddress, Item, ItemBundle, ItemVariation,
|
||||
Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.orders import OrderFee
|
||||
@@ -87,12 +88,13 @@ error_messages = {
|
||||
'addon_min_count': _('You need to select at least %(min)s add-ons from the category %(cat)s for the '
|
||||
'product %(base)s.'),
|
||||
'addon_only': _('One of the products you selected can only be bought as an add-on to another project.'),
|
||||
'bundled_only': _('One of the products you selected can only be bought part of a bundle.'),
|
||||
}
|
||||
|
||||
|
||||
class CartManager:
|
||||
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'price', 'voucher', 'quotas',
|
||||
'addon_to', 'subevent', 'includes_tax'))
|
||||
'addon_to', 'subevent', 'includes_tax', 'bundled'))
|
||||
RemoveOperation = namedtuple('RemoveOperation', ('position',))
|
||||
ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'price', 'voucher',
|
||||
'quotas', 'subevent'))
|
||||
@@ -162,7 +164,7 @@ class CartManager:
|
||||
self._items_cache.update({
|
||||
i.pk: i
|
||||
for i in self.event.items.select_related('category').prefetch_related(
|
||||
'addons', 'addons__addon_category', 'quotas'
|
||||
'addons', 'bundles', 'addons__addon_category', 'quotas'
|
||||
).filter(
|
||||
id__in=[i for i in item_ids if i and i not in self._items_cache]
|
||||
)
|
||||
@@ -215,9 +217,12 @@ class CartManager:
|
||||
raise CartError(error_messages['ended'])
|
||||
|
||||
if isinstance(op, self.AddOperation):
|
||||
if op.item.category and op.item.category.is_addon and not op.addon_to:
|
||||
if op.item.category and op.item.category.is_addon and not (op.addon_to and op.addon_to != 'FAKE'):
|
||||
raise CartError(error_messages['addon_only'])
|
||||
|
||||
if op.item.require_bundling and not op.addon_to == 'FAKE':
|
||||
raise CartError(error_messages['bundled_only'])
|
||||
|
||||
if op.item.max_per_order or op.item.min_per_order:
|
||||
new_total = (
|
||||
len([1 for p in self.positions if p.item_id == op.item.pk]) +
|
||||
@@ -246,12 +251,13 @@ class CartManager:
|
||||
|
||||
def _get_price(self, item: Item, variation: Optional[ItemVariation],
|
||||
voucher: Optional[Voucher], custom_price: Optional[Decimal],
|
||||
subevent: Optional[SubEvent], cp_is_net: bool=None):
|
||||
subevent: Optional[SubEvent], cp_is_net: bool=None, force_custom_price=False,
|
||||
bundled_sum=Decimal('0.00')):
|
||||
try:
|
||||
return get_price(
|
||||
item, variation, voucher, custom_price, subevent,
|
||||
custom_price_is_net=cp_is_net if cp_is_net is not None else self.event.settings.display_net_prices,
|
||||
invoice_address=self.invoice_address
|
||||
invoice_address=self.invoice_address, force_custom_price=force_custom_price, bundled_sum=bundled_sum
|
||||
)
|
||||
except ValueError as e:
|
||||
if str(e) == 'price_too_high':
|
||||
@@ -261,22 +267,52 @@ class CartManager:
|
||||
|
||||
def extend_expired_positions(self):
|
||||
expired = self.positions.filter(expires__lte=self.now_dt).select_related(
|
||||
'item', 'variation', 'voucher'
|
||||
).prefetch_related('item__quotas', 'variation__quotas')
|
||||
'item', 'variation', 'voucher', 'addon_to', 'addon_to__item'
|
||||
).prefetch_related(
|
||||
'item__quotas',
|
||||
'variation__quotas',
|
||||
'addons'
|
||||
).order_by('-is_bundled')
|
||||
err = None
|
||||
changed_prices = {}
|
||||
for cp in expired:
|
||||
if not cp.includes_tax:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
|
||||
cp_is_net=True)
|
||||
price = TaxedPrice(net=price.net, gross=price.net, rate=0, tax=0, name='')
|
||||
if cp.is_bundled:
|
||||
try:
|
||||
bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation)
|
||||
price = bundle.designated_price or 0
|
||||
except ItemBundle.DoesNotExist:
|
||||
price = cp.price
|
||||
|
||||
changed_prices[cp.pk] = price
|
||||
|
||||
if not cp.includes_tax:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, price, cp.subevent,
|
||||
force_custom_price=True, cp_is_net=False)
|
||||
price = TaxedPrice(net=price.net, gross=price.net, rate=0, tax=0, name='')
|
||||
else:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, price, cp.subevent,
|
||||
force_custom_price=True)
|
||||
else:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent)
|
||||
bundled_sum = Decimal('0.00')
|
||||
if not cp.addon_to_id:
|
||||
for bundledp in cp.addons.all():
|
||||
if bundledp.is_bundled:
|
||||
bundledprice = changed_prices.get(bundledp.pk, bundledp.price)
|
||||
bundled_sum += bundledprice
|
||||
|
||||
if not cp.includes_tax:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
|
||||
cp_is_net=True, bundled_sum=bundled_sum)
|
||||
price = TaxedPrice(net=price.net, gross=price.net, rate=0, tax=0, name='')
|
||||
else:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
|
||||
bundled_sum=bundled_sum)
|
||||
|
||||
quotas = list(cp.quotas)
|
||||
if not quotas:
|
||||
self._operations.append(self.RemoveOperation(position=cp))
|
||||
continue
|
||||
err = error_messages['unavailable']
|
||||
continue
|
||||
|
||||
if not cp.voucher or (not cp.voucher.allow_ignore_quota and not cp.voucher.block_quota):
|
||||
for quota in quotas:
|
||||
@@ -341,10 +377,48 @@ class CartManager:
|
||||
else:
|
||||
quotas = []
|
||||
|
||||
price = self._get_price(item, variation, voucher, i.get('price'), subevent)
|
||||
# Fetch bundled items
|
||||
bundled = []
|
||||
bundled_sum = Decimal('0.00')
|
||||
db_bundles = list(item.bundles.all())
|
||||
self._update_items_cache([b.bundled_item_id for b in db_bundles], [b.bundled_variation_id for b in db_bundles])
|
||||
for bundle in db_bundles:
|
||||
if bundle.bundled_item_id not in self._items_cache or (
|
||||
bundle.bundled_variation_id and bundle.bundled_variation_id not in self._variations_cache
|
||||
):
|
||||
raise CartError(error_messages['not_for_sale'])
|
||||
bitem = self._items_cache[bundle.bundled_item_id]
|
||||
bvar = self._variations_cache[bundle.bundled_variation_id] if bundle.bundled_variation_id else None
|
||||
bundle_quotas = list(bitem.quotas.filter(subevent=subevent)
|
||||
if bvar is None else bvar.quotas.filter(subevent=subevent))
|
||||
if not bundle_quotas:
|
||||
raise CartError(error_messages['unavailable'])
|
||||
if not voucher or not voucher.allow_ignore_quota:
|
||||
for quota in bundle_quotas:
|
||||
quota_diff[quota] += bundle.count * i['count']
|
||||
else:
|
||||
bundle_quotas = []
|
||||
|
||||
if bundle.designated_price:
|
||||
bprice = self._get_price(bitem, bvar, None, bundle.designated_price, subevent, force_custom_price=True,
|
||||
cp_is_net=False)
|
||||
else:
|
||||
bprice = TAXED_ZERO
|
||||
bundled_sum += bundle.designated_price * bundle.count
|
||||
|
||||
bop = self.AddOperation(
|
||||
count=bundle.count, item=bitem, variation=bvar, price=bprice,
|
||||
voucher=None, quotas=bundle_quotas, addon_to='FAKE', subevent=subevent,
|
||||
includes_tax=bool(bprice.rate), bundled=[]
|
||||
)
|
||||
self._check_item_constraints(bop)
|
||||
bundled.append(bop)
|
||||
|
||||
price = self._get_price(item, variation, voucher, i.get('price'), subevent, bundled_sum=bundled_sum)
|
||||
|
||||
op = self.AddOperation(
|
||||
count=i['count'], item=item, variation=variation, price=price, voucher=voucher, quotas=quotas,
|
||||
addon_to=False, subevent=subevent, includes_tax=bool(price.rate)
|
||||
addon_to=False, subevent=subevent, includes_tax=bool(price.rate), bundled=bundled
|
||||
)
|
||||
self._check_item_constraints(op)
|
||||
operations.append(op)
|
||||
@@ -403,6 +477,7 @@ class CartManager:
|
||||
current_addons[cp] = {
|
||||
(a.item_id, a.variation_id): a
|
||||
for a in cp.addons.all()
|
||||
if not a.is_bundled
|
||||
}
|
||||
|
||||
# Create operations, perform various checks
|
||||
@@ -449,7 +524,7 @@ class CartManager:
|
||||
|
||||
op = self.AddOperation(
|
||||
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
|
||||
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate)
|
||||
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[]
|
||||
)
|
||||
self._check_item_constraints(op)
|
||||
operations.append(op)
|
||||
@@ -609,11 +684,29 @@ class CartManager:
|
||||
|
||||
available_count = min(quota_available_count, voucher_available_count)
|
||||
|
||||
if isinstance(op, self.AddOperation):
|
||||
for b in op.bundled:
|
||||
b_quota_available_count = min(available_count * b.count, min(quotas_ok[q] for q in b.quotas))
|
||||
if b_quota_available_count < b.count:
|
||||
err = err or error_messages['unavailable']
|
||||
available_count = 0
|
||||
elif b_quota_available_count < available_count * b.count:
|
||||
err = err or error_messages['in_part']
|
||||
available_count = b_quota_available_count // b.count
|
||||
for q in b.quotas:
|
||||
quotas_ok[q] -= available_count * b.count
|
||||
# TODO: is this correct?
|
||||
|
||||
for q in op.quotas:
|
||||
quotas_ok[q] -= available_count
|
||||
if op.voucher:
|
||||
vouchers_ok[op.voucher] -= available_count
|
||||
|
||||
if any(qa < 0 for qa in quotas_ok.values()):
|
||||
# Safeguard, shouldn't happen
|
||||
err = err or error_messages['unavailable']
|
||||
available_count = 0
|
||||
|
||||
if isinstance(op, self.AddOperation):
|
||||
for k in range(available_count):
|
||||
cp = CartPosition(
|
||||
@@ -646,6 +739,17 @@ class CartManager:
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
if op.bundled:
|
||||
cp.save() # Needs to be in the database already so we have a PK that we can reference
|
||||
for b in op.bundled:
|
||||
for j in range(b.count):
|
||||
new_cart_positions.append(CartPosition(
|
||||
event=self.event, item=b.item, variation=b.variation,
|
||||
price=b.price.gross, expires=self._expiry, cart_id=self.cart_id,
|
||||
voucher=None, addon_to=cp,
|
||||
subevent=b.subevent, includes_tax=b.includes_tax, is_bundled=True
|
||||
))
|
||||
|
||||
new_cart_positions.append(cp)
|
||||
elif isinstance(op, self.ExtendOperation):
|
||||
if available_count == 1:
|
||||
@@ -659,10 +763,11 @@ class CartManager:
|
||||
raise AssertionError("ExtendOperation cannot affect more than one item")
|
||||
|
||||
for p in new_cart_positions:
|
||||
if p._answers:
|
||||
p.save()
|
||||
if getattr(p, '_answers', None):
|
||||
if not p.pk: # We stored some to the database already before
|
||||
p.save()
|
||||
_save_answers(p, {}, p._answers)
|
||||
CartPosition.objects.bulk_create([p for p in new_cart_positions if not p._answers])
|
||||
CartPosition.objects.bulk_create([p for p in new_cart_positions if not getattr(p, '_answers', None) and not p.pk])
|
||||
return err
|
||||
|
||||
def commit(self):
|
||||
@@ -747,7 +852,7 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
||||
"""
|
||||
Adds a list of items to a user's cart.
|
||||
:param event: The event ID in question
|
||||
:param items: A list of dicts with the keys item, variation, number, custom_price, voucher
|
||||
:param items: A list of dicts with the keys item, variation, count, custom_price, voucher
|
||||
:param cart_id: Session ID of a guest
|
||||
:raises CartError: On any error that occured
|
||||
"""
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytz
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Max, Q, Sum
|
||||
from django.db.models import Exists, F, Max, OuterRef, Q, Sum
|
||||
from django.db.models.functions import Greatest
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
@@ -26,6 +26,7 @@ from pretix.base.models import (
|
||||
OrderPosition, Quota, User, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import ItemBundle
|
||||
from pretix.base.models.orders import (
|
||||
CachedCombinedTicket, CachedTicket, InvoiceAddress, OrderFee, OrderRefund,
|
||||
generate_position_secret, generate_secret,
|
||||
@@ -400,10 +401,27 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
_check_date(event, now_dt)
|
||||
|
||||
products_seen = Counter()
|
||||
for i, cp in enumerate(positions):
|
||||
changed_prices = {}
|
||||
deleted_positions = set()
|
||||
|
||||
def delete(cp):
|
||||
# Delete a cart position, including parents and children, if applicable
|
||||
if cp.is_bundled:
|
||||
delete(cp.addon_to)
|
||||
else:
|
||||
for p in cp.addons.all():
|
||||
deleted_positions.add(p.pk)
|
||||
p.delete()
|
||||
deleted_positions.add(cp.pk)
|
||||
cp.delete()
|
||||
|
||||
for i, cp in enumerate(sorted(positions, key=lambda s: -int(s.is_bundled))):
|
||||
if cp.pk in deleted_positions:
|
||||
continue
|
||||
|
||||
if not cp.item.is_available() or (cp.variation and not cp.variation.active):
|
||||
err = err or error_messages['unavailable']
|
||||
cp.delete()
|
||||
delete(cp)
|
||||
continue
|
||||
quotas = list(cp.quotas)
|
||||
|
||||
@@ -412,7 +430,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
err = error_messages['max_items_per_product']
|
||||
errargs = {'max': cp.item.max_per_order,
|
||||
'product': cp.item.name}
|
||||
cp.delete() # Sorry!
|
||||
delete(cp)
|
||||
break
|
||||
|
||||
if cp.voucher:
|
||||
@@ -422,27 +440,27 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
v_avail = cp.voucher.max_usages - cp.voucher.redeemed - redeemed_in_carts.count()
|
||||
if v_avail < 1:
|
||||
err = err or error_messages['voucher_redeemed']
|
||||
cp.delete() # Sorry!
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.subevent and cp.subevent.presale_start and now_dt < cp.subevent.presale_start:
|
||||
err = err or error_messages['some_subevent_not_started']
|
||||
cp.delete()
|
||||
delete(cp)
|
||||
break
|
||||
|
||||
if cp.subevent and cp.subevent.presale_has_ended:
|
||||
err = err or error_messages['some_subevent_ended']
|
||||
cp.delete()
|
||||
delete(cp)
|
||||
break
|
||||
|
||||
if cp.item.require_voucher and cp.voucher is None:
|
||||
cp.delete()
|
||||
delete(cp)
|
||||
err = err or error_messages['voucher_required']
|
||||
break
|
||||
|
||||
if cp.item.hide_without_voucher and (cp.voucher is None or cp.voucher.item is None
|
||||
or cp.voucher.item.pk != cp.item.pk):
|
||||
cp.delete()
|
||||
delete(cp)
|
||||
err = error_messages['voucher_required']
|
||||
break
|
||||
|
||||
@@ -450,18 +468,34 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
# Other checks are not necessary
|
||||
continue
|
||||
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
|
||||
addon_to=cp.addon_to, invoice_address=address)
|
||||
if cp.is_bundled:
|
||||
try:
|
||||
bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation)
|
||||
bprice = bundle.designated_price or 0
|
||||
except ItemBundle.DoesNotExist:
|
||||
bprice = cp.price
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
|
||||
invoice_address=address, force_custom_price=True)
|
||||
changed_prices[cp.pk] = bprice
|
||||
else:
|
||||
bundled_sum = 0
|
||||
if not cp.addon_to_id:
|
||||
for bundledp in cp.addons.all():
|
||||
if bundledp.is_bundled:
|
||||
bundled_sum += changed_prices.get(bundledp.pk, bundledp.price)
|
||||
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
|
||||
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum)
|
||||
|
||||
if price is False or len(quotas) == 0:
|
||||
err = err or error_messages['unavailable']
|
||||
cp.delete()
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.voucher:
|
||||
if cp.voucher.valid_until and cp.voucher.valid_until < now_dt:
|
||||
err = err or error_messages['voucher_expired']
|
||||
cp.delete()
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if price.gross != cp.price and not (cp.item.free_price and cp.price > price.gross):
|
||||
@@ -494,7 +528,8 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
minutes=event.settings.get('reservation_time', as_type=int))
|
||||
cp.save()
|
||||
else:
|
||||
cp.delete() # Sorry!
|
||||
# Sorry, can't let you keep that!
|
||||
delete(cp)
|
||||
if err:
|
||||
raise OrderError(err, errargs)
|
||||
|
||||
@@ -599,7 +634,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
|
||||
with event.lock() as now_dt:
|
||||
positions = list(CartPosition.objects.filter(
|
||||
id__in=position_ids).select_related('item', 'variation', 'subevent'))
|
||||
id__in=position_ids).select_related('item', 'variation', 'subevent', 'addon_to').prefetch_related('addons'))
|
||||
if len(positions) == 0:
|
||||
raise OrderError(error_messages['empty'])
|
||||
if len(position_ids) != len(positions):
|
||||
@@ -688,7 +723,8 @@ def send_expiry_warnings(sender, **kwargs):
|
||||
today = now().replace(hour=0, minute=0, second=0)
|
||||
|
||||
for o in Order.objects.filter(
|
||||
expires__gte=today, expiry_reminder_sent=False, status=Order.STATUS_PENDING, datetime__lte=now() - timedelta(hours=2)
|
||||
expires__gte=today, expiry_reminder_sent=False, status=Order.STATUS_PENDING,
|
||||
datetime__lte=now() - timedelta(hours=2), require_approval=False
|
||||
).only('pk'):
|
||||
with transaction.atomic():
|
||||
o = Order.objects.select_related('event').select_for_update().get(pk=o.pk)
|
||||
@@ -1432,3 +1468,52 @@ def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_tok
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
raise OrderError(error_messages['busy'])
|
||||
|
||||
|
||||
def change_payment_provider(order: Order, payment_provider, amount=None, new_payment=None):
|
||||
e = OrderPayment.objects.filter(fee=OuterRef('pk'), state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
OrderPayment.PAYMENT_STATE_REFUNDED))
|
||||
open_fees = list(
|
||||
order.fees.annotate(has_p=Exists(e)).filter(
|
||||
Q(fee_type=OrderFee.FEE_TYPE_PAYMENT) & ~Q(has_p=True)
|
||||
)
|
||||
)
|
||||
if open_fees:
|
||||
fee = open_fees[0]
|
||||
if len(open_fees) > 1:
|
||||
for f in open_fees[1:]:
|
||||
f.delete()
|
||||
else:
|
||||
fee = OrderFee(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.00'), order=order)
|
||||
old_fee = fee.value
|
||||
|
||||
new_fee = payment_provider.calculate_fee(
|
||||
order.pending_sum - old_fee if amount is None else amount
|
||||
)
|
||||
with transaction.atomic():
|
||||
if new_fee:
|
||||
fee.value = new_fee
|
||||
fee.internal_type = payment_provider.identifier
|
||||
fee._calculate_tax()
|
||||
fee.save()
|
||||
else:
|
||||
if fee.pk:
|
||||
fee.delete()
|
||||
fee = None
|
||||
|
||||
open_payment = None
|
||||
if new_payment:
|
||||
lp = order.payments.exclude(pk=new_payment.pk).last()
|
||||
else:
|
||||
lp = order.payments.last()
|
||||
if lp and lp.state not in (OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED):
|
||||
open_payment = lp
|
||||
|
||||
if open_payment and open_payment.state in (OrderPayment.PAYMENT_STATE_PENDING,
|
||||
OrderPayment.PAYMENT_STATE_CREATED):
|
||||
open_payment.state = OrderPayment.PAYMENT_STATE_CANCELED
|
||||
open_payment.save(update_fields=['state'])
|
||||
|
||||
order.total = (order.positions.aggregate(sum=Sum('price'))['sum'] or 0) + (order.fees.aggregate(sum=Sum('value'))['sum'] or 0)
|
||||
order.save(update_fields=['total'])
|
||||
return old_fee, new_fee, fee
|
||||
|
||||
@@ -11,7 +11,8 @@ from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
def get_price(item: Item, variation: ItemVariation = None,
|
||||
voucher: Voucher = None, custom_price: Decimal = None,
|
||||
subevent: SubEvent = None, custom_price_is_net: bool = False,
|
||||
addon_to: AbstractPosition = None, invoice_address: InvoiceAddress = None) -> TaxedPrice:
|
||||
addon_to: AbstractPosition = None, invoice_address: InvoiceAddress = None,
|
||||
force_custom_price: bool = False, bundled_sum: Decimal = Decimal('0.00')) -> TaxedPrice:
|
||||
if addon_to:
|
||||
try:
|
||||
iao = addon_to.item.addons.get(addon_category_id=item.category_id)
|
||||
@@ -44,6 +45,11 @@ def get_price(item: Item, variation: ItemVariation = None,
|
||||
)
|
||||
price = tax_rule.tax(price)
|
||||
|
||||
if force_custom_price and custom_price is not None and custom_price != "":
|
||||
if custom_price_is_net:
|
||||
price = tax_rule.tax(custom_price, base_price_is='net')
|
||||
else:
|
||||
price = tax_rule.tax(custom_price, base_price_is='gross')
|
||||
if item.free_price and custom_price is not None and custom_price != "":
|
||||
if not isinstance(custom_price, Decimal):
|
||||
custom_price = Decimal(str(custom_price).replace(",", "."))
|
||||
@@ -54,6 +60,11 @@ def get_price(item: Item, variation: ItemVariation = None,
|
||||
else:
|
||||
price = tax_rule.tax(max(custom_price, price.gross), base_price_is='gross')
|
||||
|
||||
if bundled_sum:
|
||||
price = price - TaxedPrice(net=bundled_sum, gross=bundled_sum, rate=0, tax=0, name='')
|
||||
if price.gross < Decimal('0.00'):
|
||||
return TAXED_ZERO
|
||||
|
||||
if invoice_address and not tax_rule.tax_applicable(invoice_address):
|
||||
price.tax = Decimal('0.00')
|
||||
price.rate = Decimal('0.00')
|
||||
|
||||
@@ -31,6 +31,6 @@ def refresh_quota_caches():
|
||||
Q(cached_availability_time__isnull=True) |
|
||||
Q(cached_availability_time__lt=F('last_activity')) |
|
||||
Q(cached_availability_time__lt=now() - timedelta(hours=2), last_activity__gt=now() - timedelta(days=7))
|
||||
)
|
||||
).select_related('subevent')
|
||||
for q in quotas:
|
||||
q.availability()
|
||||
|
||||
@@ -31,12 +31,13 @@ def run_update_check(sender, **kwargs):
|
||||
@app.task
|
||||
def update_check():
|
||||
gs = GlobalSettingsObject()
|
||||
if not gs.settings.update_check_perform:
|
||||
return
|
||||
|
||||
if not gs.settings.update_check_id:
|
||||
gs.settings.set('update_check_id', uuid.uuid4().hex)
|
||||
|
||||
if not gs.settings.update_check_perform:
|
||||
return
|
||||
|
||||
if 'runserver' in sys.argv:
|
||||
gs.settings.set('update_check_last', now())
|
||||
gs.settings.set('update_check_result', {
|
||||
|
||||
@@ -73,6 +73,10 @@ DEFAULTS = {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
},
|
||||
'invoice_address_explanation_text': {
|
||||
'default': '',
|
||||
'type': LazyI18nString
|
||||
},
|
||||
'invoice_include_free': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
@@ -93,6 +97,10 @@ DEFAULTS = {
|
||||
'default': '30',
|
||||
'type': int
|
||||
},
|
||||
'redirect_to_checkout_directly': {
|
||||
'default': 'False',
|
||||
'type': bool
|
||||
},
|
||||
'payment_explanation': {
|
||||
'default': '',
|
||||
'type': LazyI18nString
|
||||
@@ -386,7 +394,7 @@ Your {event} team"""))
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
|
||||
we did not yet receive a payment for your order for {event}.
|
||||
we did not yet receive a full payment for your order for {event}.
|
||||
Please keep in mind that we only guarantee your order if we receive
|
||||
your payment before {expire_date}.
|
||||
|
||||
|
||||
@@ -78,6 +78,21 @@ def abslink_callback(attrs, new=False):
|
||||
return attrs
|
||||
|
||||
|
||||
def markdown_compile_email(source):
|
||||
return bleach.linkify(bleach.clean(
|
||||
markdown.markdown(
|
||||
source,
|
||||
extensions=[
|
||||
'markdown.extensions.sane_lists',
|
||||
# 'markdown.extensions.nl2br' # disabled for backwards-compatibility
|
||||
]
|
||||
),
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
))
|
||||
|
||||
|
||||
def markdown_compile(source):
|
||||
return bleach.clean(
|
||||
markdown.markdown(
|
||||
|
||||
@@ -17,6 +17,7 @@ from pretix.base.models import (
|
||||
|
||||
class BaseQuestionsViewMixin:
|
||||
form_class = BaseQuestionsForm
|
||||
all_optional = False
|
||||
|
||||
@staticmethod
|
||||
def _keyfunc(pos):
|
||||
@@ -47,6 +48,7 @@ class BaseQuestionsViewMixin:
|
||||
prefix=cr.id,
|
||||
cartpos=cartpos,
|
||||
orderpos=orderpos,
|
||||
all_optional=self.all_optional,
|
||||
data=(self.request.POST if self.request.method == 'POST' else None),
|
||||
files=(self.request.FILES if self.request.method == 'POST' else None))
|
||||
form.pos = cartpos or orderpos
|
||||
@@ -154,7 +156,7 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
|
||||
@cached_property
|
||||
def positions(self):
|
||||
qqs = Question.objects.all()
|
||||
qqs = self.request.event.questions.all()
|
||||
if self.only_user_visible:
|
||||
qqs = qqs.filter(ask_during_checkin=False)
|
||||
return list(self.order.positions.select_related(
|
||||
@@ -173,7 +175,7 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
),
|
||||
).select_related('dependency_question'),
|
||||
to_attr='questions_to_ask')
|
||||
))
|
||||
|
||||
|
||||
@@ -627,6 +627,13 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
"products."),
|
||||
required=False
|
||||
)
|
||||
invoice_address_explanation_text = I18nFormField(
|
||||
label=_("Invoice address explanation"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("This text will be shown above the invoice address form during checkout.")
|
||||
)
|
||||
invoice_numbers_consecutive = forms.BooleanField(
|
||||
label=_("Generate invoices with consecutive numbers"),
|
||||
help_text=_("If deactivated, the order code will be used in the invoice number."),
|
||||
@@ -1067,6 +1074,10 @@ class DisplaySettingsForm(SettingsForm):
|
||||
label=_('Ask search engines not to index the ticket shop'),
|
||||
required=False
|
||||
)
|
||||
redirect_to_checkout_directly = forms.BooleanField(
|
||||
label=_('Directly redirect to check-out after a product has been added to the cart.'),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = kwargs['obj']
|
||||
@@ -1185,7 +1196,7 @@ class TaxRuleForm(I18nModelForm):
|
||||
class WidgetCodeForm(forms.Form):
|
||||
subevent = forms.ModelChoiceField(
|
||||
label=pgettext_lazy('subevent', "Date"),
|
||||
required=True,
|
||||
required=False,
|
||||
queryset=SubEvent.objects.none()
|
||||
)
|
||||
language = forms.ChoiceField(
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import datetime
|
||||
|
||||
import dateutil
|
||||
import pytz
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Max
|
||||
from django.forms.formsets import DELETION_FIELD_NAME
|
||||
from django.forms.utils import from_current_timezone, to_current_timezone
|
||||
from django.urls import reverse
|
||||
from django.utils import formats
|
||||
from django.utils.translation import (
|
||||
pgettext_lazy, ugettext as __, ugettext_lazy as _,
|
||||
)
|
||||
@@ -13,7 +19,7 @@ from pretix.base.forms import I18nFormSet, I18nModelForm
|
||||
from pretix.base.models import (
|
||||
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
|
||||
)
|
||||
from pretix.base.models.items import ItemAddOn
|
||||
from pretix.base.models.items import ItemAddOn, ItemBundle
|
||||
from pretix.base.signals import item_copy_data
|
||||
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
||||
from pretix.control.forms.widgets import Select2
|
||||
@@ -33,17 +39,103 @@ class CategoryForm(I18nModelForm):
|
||||
]
|
||||
|
||||
|
||||
class AdjustableTypeField(forms.Textarea):
|
||||
def __init__(self, *args, type=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def format_value(self, value):
|
||||
if getattr(self, 'type') == 'W' and value:
|
||||
return str(formats.localize_input(to_current_timezone(dateutil.parser.parse(value))))
|
||||
elif getattr(self, 'type') == 'D' and value:
|
||||
return str(formats.localize_input(dateutil.parser.parse(value).date()))
|
||||
elif getattr(self, 'type') == 'T' and value:
|
||||
return str(formats.localize_input(dateutil.parser.parse(value).time()))
|
||||
return super().format_value(value)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
if 'type' in data and data['type'] == 'W' and 'default_value_date' in data and 'default_value_time' in data:
|
||||
d_date = d_time = None
|
||||
|
||||
for format in formats.get_format('DATE_INPUT_FORMATS'):
|
||||
try:
|
||||
d_date = datetime.datetime.strptime(data['default_value_date'], format).date()
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
for format in formats.get_format('TIME_INPUT_FORMATS'):
|
||||
try:
|
||||
d_time = datetime.datetime.strptime(data['default_value_time'], format).time()
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
if d_date and d_time:
|
||||
return from_current_timezone(datetime.datetime.combine(d_date, d_time)).astimezone(pytz.UTC).isoformat()
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
val = super().value_from_datadict(data, files, name)
|
||||
if 'type' in data and data['type'] == 'D' and val:
|
||||
for format in formats.get_format('DATE_INPUT_FORMATS'):
|
||||
try:
|
||||
d_date = datetime.datetime.strptime(val, format).date()
|
||||
val = d_date.isoformat()
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
elif 'type' in data and data['type'] == 'T' and val:
|
||||
for format in formats.get_format('TIME_INPUT_FORMATS'):
|
||||
try:
|
||||
d_date = datetime.datetime.strptime(val, format).time()
|
||||
val = d_date.isoformat()
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
return val
|
||||
|
||||
|
||||
class QuestionForm(I18nModelForm):
|
||||
question = I18nFormField(
|
||||
label=_("Question"),
|
||||
widget_kwargs={'attrs': {'rows': 5}},
|
||||
widget_kwargs={'attrs': {'rows': 2}},
|
||||
widget=I18nTextarea
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['items'].queryset = self.instance.event.items.all()
|
||||
self.fields['dependency_question'].queryset = self.instance.event.questions.filter(
|
||||
type__in=(Question.TYPE_BOOLEAN, Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE)
|
||||
)
|
||||
if self.instance.pk:
|
||||
self.fields['dependency_question'].queryset = self.fields['dependency_question'].queryset.exclude(
|
||||
pk=self.instance.pk
|
||||
)
|
||||
self.fields['identifier'].required = False
|
||||
if self.instance:
|
||||
self.fields['default_value'].widget.type = self.instance.type
|
||||
self.fields['help_text'].widget.attrs['rows'] = 3
|
||||
|
||||
def clean_dependency_question(self):
|
||||
dep = val = self.cleaned_data.get('dependency_question')
|
||||
if dep:
|
||||
seen_ids = {self.instance.pk} if self.instance else set()
|
||||
while dep:
|
||||
if dep.pk in seen_ids:
|
||||
raise ValidationError(_('Circular dependency between questions detected.'))
|
||||
seen_ids.add(dep.pk)
|
||||
dep = dep.dependency_question
|
||||
return val
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d.get('dependency_question') and not d.get('dependency_value'):
|
||||
raise ValidationError({'dependency_value': [_('This field is required')]})
|
||||
if d.get('dependency_question') and d.get('ask_during_checkin'):
|
||||
raise ValidationError(_('Dependencies between questions are not supported during check-in.'))
|
||||
return d
|
||||
|
||||
class Meta:
|
||||
model = Question
|
||||
@@ -53,14 +145,19 @@ class QuestionForm(I18nModelForm):
|
||||
'help_text',
|
||||
'type',
|
||||
'required',
|
||||
'default_value',
|
||||
'ask_during_checkin',
|
||||
'identifier',
|
||||
'items'
|
||||
'items',
|
||||
'dependency_question',
|
||||
'dependency_value'
|
||||
]
|
||||
widgets = {
|
||||
'items': forms.CheckboxSelectMultiple(
|
||||
attrs={'class': 'scrolling-multiple-choice'}
|
||||
),
|
||||
'default_value': AdjustableTypeField(),
|
||||
'dependency_value': forms.Select
|
||||
}
|
||||
|
||||
|
||||
@@ -270,6 +367,12 @@ class ItemCreateForm(I18nModelForm):
|
||||
if self.cleaned_data.get('copy_from'):
|
||||
for question in self.cleaned_data['copy_from'].questions.all():
|
||||
question.items.add(instance)
|
||||
for a in self.cleaned_data['copy_from'].addons.all():
|
||||
instance.addons.create(addon_category=a.addon_category, min_count=a.min_count, max_count=a.max_count,
|
||||
price_included=a.price_included, position=a.position)
|
||||
for b in self.cleaned_data['copy_from'].bundles.all():
|
||||
instance.bundles.create(bundled_item=b.bundled_item, bundled_variation=b.bundled_variation,
|
||||
count=b.count, designated_price=b.designated_price)
|
||||
|
||||
item_copy_data.send(sender=self.event, source=self.cleaned_data['copy_from'], target=instance)
|
||||
|
||||
@@ -326,6 +429,7 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'over 65. This ticket includes access to all parts of the event, except the VIP '
|
||||
'area.'
|
||||
)
|
||||
self.fields['description'].widget.attrs['rows'] = '4'
|
||||
self.fields['sales_channels'] = forms.MultipleChoiceField(
|
||||
label=_('Sales channels'),
|
||||
choices=(
|
||||
@@ -360,7 +464,8 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'min_per_order',
|
||||
'checkin_attention',
|
||||
'generate_tickets',
|
||||
'original_price'
|
||||
'original_price',
|
||||
'require_bundling',
|
||||
]
|
||||
field_classes = {
|
||||
'available_from': SplitDateTimeField,
|
||||
@@ -491,3 +596,100 @@ class ItemAddOnForm(I18nModelForm):
|
||||
'min_count': _('Be aware that setting a minimal number makes it impossible to buy this product if all '
|
||||
'available add-ons are sold out.')
|
||||
}
|
||||
|
||||
|
||||
class ItemBundleFormSet(I18nFormSet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.get('event')
|
||||
self.item = kwargs.pop('item')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _construct_form(self, i, **kwargs):
|
||||
kwargs['event'] = self.event
|
||||
kwargs['item'] = self.item
|
||||
return super()._construct_form(i, **kwargs)
|
||||
|
||||
@property
|
||||
def empty_form(self):
|
||||
self.is_valid()
|
||||
form = self.form(
|
||||
auto_id=self.auto_id,
|
||||
prefix=self.add_prefix('__prefix__'),
|
||||
empty_permitted=True,
|
||||
use_required_attribute=False,
|
||||
locales=self.locales,
|
||||
item=self.item,
|
||||
event=self.event
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
|
||||
|
||||
class ItemBundleForm(I18nModelForm):
|
||||
itemvar = forms.ChoiceField(label=_('Bundled product'))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.item = kwargs.pop('item')
|
||||
super().__init__(*args, **kwargs)
|
||||
instance = kwargs.get('instance', None)
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
if instance:
|
||||
try:
|
||||
if instance.bundled_variation:
|
||||
initial['itemvar'] = '%d-%d' % (instance.bundled_item.pk, instance.bundled_variation.pk)
|
||||
elif instance.bundled_item:
|
||||
initial['itemvar'] = str(instance.bundled_item.pk)
|
||||
except Item.DoesNotExist:
|
||||
pass
|
||||
|
||||
kwargs['initial'] = initial
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
choices = []
|
||||
for i in self.event.items.prefetch_related('variations').all():
|
||||
pname = str(i)
|
||||
if not i.is_available():
|
||||
pname += ' ({})'.format(_('inactive'))
|
||||
variations = list(i.variations.all())
|
||||
|
||||
if variations:
|
||||
for v in variations:
|
||||
choices.append(('%d-%d' % (i.pk, v.pk),
|
||||
'%s – %s' % (pname, v.value)))
|
||||
else:
|
||||
choices.append((str(i.pk), '%s' % pname))
|
||||
self.fields['itemvar'].choices = choices
|
||||
change_decimal_field(self.fields['designated_price'], self.event.currency)
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if 'itemvar' in self.cleaned_data:
|
||||
if '-' in self.cleaned_data['itemvar']:
|
||||
itemid, varid = self.cleaned_data['itemvar'].split('-')
|
||||
else:
|
||||
itemid, varid = self.cleaned_data['itemvar'], None
|
||||
|
||||
item = Item.objects.get(pk=itemid, event=self.event)
|
||||
if varid:
|
||||
variation = ItemVariation.objects.get(pk=varid, item=item)
|
||||
else:
|
||||
variation = None
|
||||
|
||||
if item == self.item:
|
||||
raise ValidationError(_("The bundled item must not be the same item as the bundling one."))
|
||||
if item.bundles.exists():
|
||||
raise ValidationError(_("The bundled item must not have bundles on its own."))
|
||||
|
||||
self.instance.bundled_item = item
|
||||
self.instance.bundled_variation = variation
|
||||
|
||||
return d
|
||||
|
||||
class Meta:
|
||||
model = ItemBundle
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'count',
|
||||
'designated_price',
|
||||
]
|
||||
|
||||
@@ -211,6 +211,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.order.payment.started': _('Payment {local_id} has been started.'),
|
||||
'pretix.event.order.payment.failed': _('Payment {local_id} has failed.'),
|
||||
'pretix.event.order.quotaexceeded': _('The order could not be marked as paid: {message}'),
|
||||
'pretix.event.order.overpaid': _('The order has been overpaid.'),
|
||||
'pretix.event.order.refund.created': _('Refund {local_id} has been created.'),
|
||||
'pretix.event.order.refund.created.externally': _('Refund {local_id} has been created by an external entity.'),
|
||||
'pretix.event.order.refund.requested': _('The customer requested you to issue a refund.'),
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<script type="text/javascript" src="{% static "charts/morris.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "clipboard/clipboard.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "rrule/rrule.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/questions.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/clipboard.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/menu.js" %}"></script>
|
||||
@@ -70,7 +71,10 @@
|
||||
</head>
|
||||
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}"
|
||||
data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}"
|
||||
data-pretixlocale="{{ request.LANGUAGE_CODE }}"
|
||||
data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}"
|
||||
{% if request.organizer %}data-organizer="{{ request.organizer.slug }}"{% endif %}
|
||||
{% if request.event %}data-event="{{ request.event.slug }}"{% endif %}
|
||||
data-select2-locale="{{ select2locale }}" data-longdateformat="{{ js_long_date_format }}" class="nojs">
|
||||
<div id="wrapper">
|
||||
<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if payment_info %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Payment provider" %}</dt>
|
||||
<dd>SumUp</dd>
|
||||
<dt>{% trans "Transaction Code" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.tx_code}}</dd>
|
||||
<dt>{% trans "Merchant Code" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.merchant_code }}</dd>
|
||||
<dt>{% trans "Currency" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.currency }}</dd>
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.status }}</dd>
|
||||
<dt>{% trans "Type" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.type }}</dd>
|
||||
<dt>{% trans "Card Entry Mode" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.entry_mode }}</dd>
|
||||
<dt>{% trans "Card number" %}</dt>
|
||||
<dd><i class="fa fa-cc-{{ payment_info.payment_data.card_type|lower }}"></i> **** **** **** {{ payment_info.payment_data.last4 }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
@@ -70,6 +70,7 @@
|
||||
<a href="?{% url_replace request 'ordering' 'email'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Name" %} <a href="?{% url_replace request 'ordering' '-name'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'name'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Ticket code" %}</th>
|
||||
<th>{% trans "Status" %} <a href="?{% url_replace request 'ordering' '-status'%}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'status'%}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Timestamp" %} <a href="?{% url_replace request 'ordering' '-timestamp'%}"><i class="fa fa-caret-down"></i></a>
|
||||
@@ -102,6 +103,9 @@
|
||||
{{ e.attendee_name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ e.secret|slice:":10" }}…
|
||||
</td>
|
||||
<td>
|
||||
{% if not e.last_checked_in %}
|
||||
<span class="label label-danger">{% trans "Not checked in" %}</span>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
{% if form.frontpage_subevent_ordering %}
|
||||
{% bootstrap_field form.frontpage_subevent_ordering layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.redirect_to_checkout_directly layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Shop design" %}</legend>
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
{% bootstrap_field form.invoice_additional_text layout="control" %}
|
||||
{% bootstrap_field form.invoice_footer_text layout="control" %}
|
||||
{% bootstrap_field form.invoice_logo_image layout="control" %}
|
||||
{% bootstrap_field form.invoice_address_explanation_text layout="control" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-default btn-lg" name="preview" value="preview" formtarget="_blank">
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
{% trans "Add-Ons" %}
|
||||
</a>
|
||||
</li>
|
||||
<li {% if "event.item.bundles" == url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:event.item.bundles' organizer=request.event.organizer.slug event=request.event.slug item=object.id %}">
|
||||
{% trans "Bundled products" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<h1>{% trans "Create product" %}</h1>
|
||||
|
||||
80
src/pretix/control/templates/pretixcontrol/item/bundles.html
Normal file
80
src/pretix/control/templates/pretixcontrol/item/bundles.html
Normal file
@@ -0,0 +1,80 @@
|
||||
{% extends "pretixcontrol/item/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load formset_tags %}
|
||||
{% block inside %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
With bundles, you can specify products that are always automatically added as add-ons in the cart for this product.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<form class="form-horizontal branches" method="post" action="">
|
||||
{% csrf_token %}
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<h3 class="panel-title">{% trans "Bundled product" %}</h3>
|
||||
</div>
|
||||
<div class="col-sm-4 text-right">
|
||||
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.itemvar layout="control" %}
|
||||
{% bootstrap_field form.count layout="control" %}
|
||||
{% bootstrap_field form.designated_price layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<h3 class="panel-title">{% trans "Bundled product" %}</h3>
|
||||
</div>
|
||||
<div class="col-sm-4 text-right">
|
||||
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field formset.empty_form.itemvar layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.count layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.designated_price layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new bundled product" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -33,6 +33,7 @@
|
||||
{% bootstrap_field form.min_per_order layout="control" %}
|
||||
{% bootstrap_field form.require_voucher layout="control" %}
|
||||
{% bootstrap_field form.hide_without_voucher layout="control" %}
|
||||
{% bootstrap_field form.require_bundling layout="control" %}
|
||||
{% bootstrap_field form.allow_cancel layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
||||
@@ -21,15 +21,9 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.question layout="control" %}
|
||||
{% bootstrap_field form.help_text layout="control" %}
|
||||
{% bootstrap_field form.type layout="control" %}
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% bootstrap_field form.ask_during_checkin layout="control" %}
|
||||
{% bootstrap_field form.required layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Apply to products" %}</legend>
|
||||
{% bootstrap_field form.items layout="control" %}
|
||||
{% bootstrap_field form.required layout="control" %}
|
||||
</fieldset>
|
||||
<div class="alert alert-info alert-required-boolean">
|
||||
{% blocktrans trimmed %}
|
||||
@@ -110,6 +104,27 @@
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
{% bootstrap_field form.help_text layout="control" %}
|
||||
{% bootstrap_field form.default_value layout="control" %}
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% bootstrap_field form.ask_during_checkin layout="control" %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_dependency_question">
|
||||
{% trans "Question dependency" %}
|
||||
<br><span class="optional">{% trans "Optional" context "form" %}</span>
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.dependency_question layout="inline" form_group_class="inner" %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<script type="text/plain" id="dependency_value_val">{{ form.instance.dependency_value }}</script>
|
||||
{% bootstrap_field form.dependency_value layout="inline" form_group_class="inner" %}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -22,13 +22,10 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "E-mail preview" %}</legend>
|
||||
<div class="tab-pane mail-preview-group">
|
||||
<pre lang="" class="mail-preview">
|
||||
{% for segment in preview_output %}
|
||||
{% spaceless %}
|
||||
{{ segment|linebreaksbr }}
|
||||
{% endspaceless %}
|
||||
{% endfor %}
|
||||
</pre>
|
||||
<div lang="{{ order.locale }}" class="mail-preview">
|
||||
<strong>{{ preview_output.subject }}</strong><br><br>
|
||||
{{ preview_output.html|safe }}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<table class="table table-condensed table-hover table-orders">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Order code" %}
|
||||
@@ -147,6 +147,32 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% if sums %}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>{% trans "Sum over all pages" %}</th>
|
||||
<th></th>
|
||||
<th>
|
||||
{% blocktrans trimmed count s=sums.c %}
|
||||
1 order
|
||||
{% plural %}
|
||||
{{ s }} orders
|
||||
{% endblocktrans %}
|
||||
</th>
|
||||
<th class="text-right">
|
||||
{% if sums.s|default_if_none:"none" != "none" %}
|
||||
{{ sums.s|money:request.event.currency }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<th class="text-right">
|
||||
{% if sums.pc %}
|
||||
{{ sums.pc }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
|
||||
@@ -34,13 +34,11 @@
|
||||
</div>
|
||||
{% bootstrap_field form.codes layout="control" %}
|
||||
|
||||
{% bootstrap_field form.max_usages layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Voucher details" %}</legend>
|
||||
{% bootstrap_field form.max_usages layout="control" %}
|
||||
{% bootstrap_field form.valid_until layout="control" %}
|
||||
{% bootstrap_field form.block_quota layout="control" %}
|
||||
{% bootstrap_field form.allow_ignore_quota layout="control" %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_tag">{% trans "Price effect" %}</label>
|
||||
<div class="col-md-5">
|
||||
@@ -67,6 +65,11 @@
|
||||
{% if form.subevent %}
|
||||
{% bootstrap_field form.subevent layout="control" %}
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
{% bootstrap_field form.block_quota layout="control" %}
|
||||
{% bootstrap_field form.allow_ignore_quota layout="control" %}
|
||||
{% bootstrap_field form.tag layout="control" %}
|
||||
{% bootstrap_field form.comment layout="control" %}
|
||||
</fieldset>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load eventsignal %}
|
||||
{% load eventurl %}
|
||||
{% block title %}{% trans "Voucher" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Voucher" %}</h1>
|
||||
@@ -25,10 +26,21 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Voucher details" %}</legend>
|
||||
{% bootstrap_field form.code layout="control" %}
|
||||
{% if voucher.pk %}
|
||||
{% if not request.event.has_subevents or voucher.subevent %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_url">{% trans "Voucher link" %}</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" name="url"
|
||||
value="{% abseventurl request.event "presale:event.redeem" %}?voucher={{ voucher.code }}&subevent={{ voucher.subevent_id }}"
|
||||
class="form-control"
|
||||
id="id_url" readonly>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.max_usages layout="control" %}
|
||||
{% bootstrap_field form.valid_until layout="control" %}
|
||||
{% bootstrap_field form.block_quota layout="control" %}
|
||||
{% bootstrap_field form.allow_ignore_quota layout="control" %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_tag">{% trans "Price effect" %}</label>
|
||||
<div class="col-md-5">
|
||||
@@ -55,6 +67,11 @@
|
||||
{% if form.subevent %}
|
||||
{% bootstrap_field form.subevent layout="control" %}
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
{% bootstrap_field form.block_quota layout="control" %}
|
||||
{% bootstrap_field form.allow_ignore_quota layout="control" %}
|
||||
{% bootstrap_field form.tag layout="control" %}
|
||||
{% bootstrap_field form.comment layout="control" %}
|
||||
</fieldset>
|
||||
|
||||
@@ -150,6 +150,8 @@ urlpatterns = [
|
||||
name='event.item.variations'),
|
||||
url(r'^items/(?P<item>\d+)/addons', item.ItemAddOns.as_view(),
|
||||
name='event.item.addons'),
|
||||
url(r'^items/(?P<item>\d+)/bundles', item.ItemBundles.as_view(),
|
||||
name='event.item.bundles'),
|
||||
url(r'^items/(?P<item>\d+)/up$', item.item_move_up, name='event.items.up'),
|
||||
url(r'^items/(?P<item>\d+)/down$', item.item_move_down, name='event.items.down'),
|
||||
url(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'),
|
||||
|
||||
@@ -158,7 +158,7 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
cl.subevent.event = self.request.event # re-use same event object to make sure settings are cached
|
||||
cl.checkin_count = annotations.get(cl.pk, {}).get('checkin_count', 0)
|
||||
cl.position_count = annotations.get(cl.pk, {}).get('position_count', 0)
|
||||
cl.percent_count = annotations.get(cl.pk, {}).get('percent_count', 0)
|
||||
cl.percent = annotations.get(cl.pk, {}).get('percent', 0)
|
||||
ctx['checkinlists'] = clists
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import bleach
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -29,6 +28,7 @@ from django.views.generic.detail import SingleObjectMixin
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from pytz import timezone
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.i18n import LazyCurrencyNumber
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Event, LogEntry, Order, RequiredAction,
|
||||
@@ -39,7 +39,7 @@ from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import build_preview_invoice_pdf
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.rich_text import markdown_compile
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.control.forms.event import (
|
||||
CancelSettingsForm, CommentForm, DisplaySettingsForm, EventDeleteForm,
|
||||
EventMetaValueForm, EventSettingsForm, EventUpdateForm,
|
||||
@@ -54,8 +54,8 @@ from pretix.multidomain.urlreverse import get_domain
|
||||
from pretix.plugins.stripe.payment import StripeSettingsHolder
|
||||
from pretix.presale.style import regenerate_css
|
||||
|
||||
from . import CreateView, PaginationMixin, UpdateView
|
||||
from ..logdisplay import OVERVIEW_BLACKLIST
|
||||
from . import CreateView, PaginationMixin, UpdateView
|
||||
|
||||
|
||||
class EventSettingsViewMixin:
|
||||
@@ -664,9 +664,9 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
idx = matched.group('idx')
|
||||
if idx in self.supported_locale:
|
||||
with translation.override(self.supported_locale[idx]):
|
||||
msgs[self.supported_locale[idx]] = bleach.linkify(markdown_compile(
|
||||
msgs[self.supported_locale[idx]] = markdown_compile_email(
|
||||
v.format_map(self.placeholders(preview_item))
|
||||
))
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'item': preview_item,
|
||||
@@ -902,6 +902,8 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
|
||||
'created by plug-ins) do not allow it.'))
|
||||
else:
|
||||
request.event.cache.set('complain_testmode_orders', False, 30)
|
||||
request.event.cartposition_set.filter(addon_to__isnull=False).delete()
|
||||
request.event.cartposition_set.all().delete()
|
||||
messages.success(self.request, _('We\'ve disabled test mode for you. Let\'s sell some real tickets!'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
@@ -1350,6 +1352,7 @@ class QuickSetupView(FormView):
|
||||
tax_rule=tax_rule,
|
||||
admission=True,
|
||||
position=i,
|
||||
sales_channels=[k for k in get_all_sales_channels().keys()]
|
||||
)
|
||||
item.log_action('pretix.event.item.added', user=self.request.user, data=dict(f.cleaned_data))
|
||||
if f.cleaned_data['quota'] or not form.cleaned_data['total_quota']:
|
||||
|
||||
@@ -22,11 +22,11 @@ from pretix.base.models import (
|
||||
QuestionAnswer, QuestionOption, Quota, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import ItemAddOn
|
||||
from pretix.base.models.items import ItemAddOn, ItemBundle
|
||||
from pretix.control.forms.item import (
|
||||
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemCreateForm,
|
||||
ItemUpdateForm, ItemVariationForm, ItemVariationsFormSet, QuestionForm,
|
||||
QuestionOptionForm, QuotaForm,
|
||||
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
|
||||
ItemBundleFormSet, ItemCreateForm, ItemUpdateForm, ItemVariationForm,
|
||||
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm,
|
||||
)
|
||||
from pretix.control.permissions import (
|
||||
EventPermissionRequiredMixin, event_permission_required,
|
||||
@@ -1043,6 +1043,92 @@ class ItemAddOns(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
class ItemBundles(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
permission = 'can_change_items'
|
||||
template_name = 'pretixcontrol/item/bundles.html'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.item = None
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
formsetclass = inlineformset_factory(
|
||||
Item, ItemBundle,
|
||||
form=ItemBundleForm, formset=ItemBundleFormSet,
|
||||
fk_name='base_item',
|
||||
can_order=False, can_delete=True, extra=0
|
||||
)
|
||||
return formsetclass(self.request.POST if self.request.method == "POST" else None,
|
||||
queryset=ItemBundle.objects.filter(base_item=self.get_object()),
|
||||
event=self.request.event, item=self.item)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
if self.formset.is_valid():
|
||||
for form in self.formset.deleted_forms:
|
||||
if not form.instance.pk:
|
||||
continue
|
||||
self.get_object().log_action(
|
||||
'pretix.event.item.bundles.removed', user=self.request.user, data={
|
||||
'bundled_item': form.instance.bundled_item.pk,
|
||||
'bundled_variation': (form.instance.bundled_variation.pk if form.instance.bundled_variation else None),
|
||||
'count': form.instance.count,
|
||||
'designated_price': str(form.instance.designated_price),
|
||||
}
|
||||
)
|
||||
form.instance.delete()
|
||||
form.instance.pk = None
|
||||
|
||||
forms = [
|
||||
ef for ef in self.formset.forms
|
||||
if ef not in self.formset.deleted_forms
|
||||
]
|
||||
for i, form in enumerate(forms):
|
||||
form.instance.base_item = self.get_object()
|
||||
created = not form.instance.pk
|
||||
form.save()
|
||||
if form.has_changed():
|
||||
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
|
||||
change_data['id'] = form.instance.pk
|
||||
self.get_object().log_action(
|
||||
'pretix.event.item.bundles.changed' if not created else
|
||||
'pretix.event.item.bundles.added',
|
||||
user=self.request.user, data=change_data
|
||||
)
|
||||
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return redirect(self.get_success_url())
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if self.get_object().category and self.get_object().category.is_addon:
|
||||
messages.error(self.request, _('You cannot add bundles to a product that is only available as an add-on '
|
||||
'itself.'))
|
||||
return redirect(self.get_previous_url())
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_previous_url(self) -> str:
|
||||
return reverse('control:event.item', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'item': self.get_object().id,
|
||||
})
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.item.bundles', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'item': self.get_object().id,
|
||||
})
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['formset'] = self.formset
|
||||
return context
|
||||
|
||||
|
||||
class ItemDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Item
|
||||
template_name = 'pretixcontrol/item/delete.html'
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.contrib import messages
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Count, IntegerField, OuterRef, ProtectedError, Subquery,
|
||||
Count, IntegerField, OuterRef, Prefetch, ProtectedError, Q, Subquery, Sum,
|
||||
)
|
||||
from django.http import (
|
||||
FileResponse, Http404, HttpResponseNotAllowed, JsonResponse,
|
||||
@@ -60,6 +60,7 @@ from pretix.base.signals import (
|
||||
register_data_exporters, register_ticket_outputs,
|
||||
)
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.base.views.mixins import OrderQuestionsViewMixin
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.forms.filter import EventOrderFilterForm, RefundFilterForm
|
||||
@@ -125,6 +126,15 @@ class OrderList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
o.has_external_refund = annotated.get(o.pk)['has_external_refund']
|
||||
o.has_pending_refund = annotated.get(o.pk)['has_pending_refund']
|
||||
|
||||
if ctx['page_obj'].paginator.count < 1000:
|
||||
# Performance safeguard: Only count positions if the data set is small
|
||||
ctx['sums'] = self.get_queryset().annotate(
|
||||
pcnt=Subquery(s, output_field=IntegerField())
|
||||
).aggregate(
|
||||
s=Sum('total'), pc=Sum('pcnt'), c=Count('id')
|
||||
)
|
||||
else:
|
||||
ctx['sums'] = self.get_queryset().aggregate(s=Sum('total'), c=Count('id'))
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
@@ -224,7 +234,9 @@ class OrderDetail(OrderView):
|
||||
).select_related(
|
||||
'item', 'variation', 'addon_to', 'tax_rule'
|
||||
).prefetch_related(
|
||||
'item__questions', 'answers', 'answers__question', 'checkins', 'checkins__list'
|
||||
'item__questions',
|
||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
|
||||
'checkins', 'checkins__list'
|
||||
).order_by('positionid')
|
||||
|
||||
positions = []
|
||||
@@ -1043,33 +1055,11 @@ class OrderResendLink(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
with language(self.order.locale):
|
||||
try:
|
||||
try:
|
||||
invoice_name = self.order.invoice_address.name
|
||||
invoice_company = self.order.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
email_template = self.order.event.settings.mail_text_resend_link
|
||||
email_context = {
|
||||
'event': self.order.event.name,
|
||||
'url': build_absolute_uri(self.order.event, 'presale:event.order', kwargs={
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret
|
||||
}),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
}
|
||||
email_subject = _('Your order: %(code)s') % {'code': self.order.code}
|
||||
self.order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.resend', user=self.request.user,
|
||||
attach_tickets=True
|
||||
)
|
||||
except SendMailException:
|
||||
messages.error(self.request, _('There was an error sending the mail. Please try again later.'))
|
||||
return redirect(self.get_order_url())
|
||||
try:
|
||||
self.order.resend_link(user=self.request.user)
|
||||
except SendMailException:
|
||||
messages.error(self.request, _('There was an error sending the mail. Please try again later.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
messages.success(self.request, _('The email has been queued to be sent.'))
|
||||
return redirect(self.get_order_url())
|
||||
@@ -1483,10 +1473,10 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
|
||||
email_template = LazyI18nString(form.cleaned_data['message'])
|
||||
email_content = render_mail(email_template, email_context)
|
||||
if self.request.POST.get('action') == 'preview':
|
||||
self.preview_output = []
|
||||
self.preview_output.append(
|
||||
_('Subject: {subject}').format(subject=form.cleaned_data['subject']))
|
||||
self.preview_output.append(email_content)
|
||||
self.preview_output = {
|
||||
'subject': _('Subject: {subject}').format(subject=form.cleaned_data['subject']),
|
||||
'html': markdown_compile_email(email_content)
|
||||
}
|
||||
return self.get(self.request, *self.args, **self.kwargs)
|
||||
else:
|
||||
try:
|
||||
@@ -1601,6 +1591,13 @@ class OrderGo(EventPermissionRequiredMixin, View):
|
||||
return redirect('control:event.order', event=request.event.slug, organizer=request.event.organizer.slug,
|
||||
code=order.code)
|
||||
except Order.DoesNotExist:
|
||||
try:
|
||||
i = self.request.event.invoices.get(Q(invoice_no=code) | Q(full_invoice_no=code))
|
||||
return redirect('control:event.order', event=request.event.slug, organizer=request.event.organizer.slug,
|
||||
code=i.order.code)
|
||||
except Invoice.DoesNotExist:
|
||||
pass
|
||||
|
||||
messages.error(request, _('There is no order with the given order code.'))
|
||||
return redirect('control:event.orders', event=request.event.slug, organizer=request.event.organizer.slug)
|
||||
|
||||
|
||||
@@ -82,7 +82,10 @@ class OrderSearch(PaginationMixin, ListView):
|
||||
|
||||
page = self.kwargs.get(self.page_kwarg) or self.request.GET.get(self.page_kwarg) or 1
|
||||
limit = self.get_paginate_by(None)
|
||||
offset = (page - 1) * limit
|
||||
try:
|
||||
offset = (int(page) - 1) * limit
|
||||
except ValueError:
|
||||
offset = 0
|
||||
resultids = list(qs.order_by().values_list('id', flat=True)[:201])
|
||||
if len(resultids) <= 200 and len(resultids) <= offset + limit:
|
||||
qs = Order.objects.using(settings.DATABASE_REPLICA).filter(
|
||||
|
||||
@@ -117,6 +117,7 @@ class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
else:
|
||||
self.object.log_action('pretix.subevent.deleted', user=self.request.user)
|
||||
self.object.cartposition_set.all().delete()
|
||||
self.object.delete()
|
||||
messages.success(request, pgettext_lazy('subevent', 'The selected date has been deleted.'))
|
||||
return HttpResponseRedirect(success_url)
|
||||
@@ -511,6 +512,7 @@ class SubEventBulkAction(EventPermissionRequiredMixin, View):
|
||||
elif request.POST.get('action') == 'delete_confirm':
|
||||
for obj in self.objects:
|
||||
if obj.allow_delete():
|
||||
obj.cartposition_set.all().delete()
|
||||
obj.log_action('pretix.subevent.deleted', user=self.request.user)
|
||||
obj.delete()
|
||||
else:
|
||||
|
||||
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: 2019-02-20 16:52+0000\n"
|
||||
"POT-Creation-Date: 2019-03-22 14:50+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
@@ -100,7 +100,7 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:193
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:28
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:20
|
||||
msgid "Close message"
|
||||
msgstr ""
|
||||
|
||||
@@ -168,44 +168,56 @@ msgstr ""
|
||||
msgid "Generating messages …"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:43
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:55
|
||||
msgid "Unknown error."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:205
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:217
|
||||
msgid "Your color has great contrast and is very easy to read!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:209
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:221
|
||||
msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:213
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:225
|
||||
msgid ""
|
||||
"Your color has bad contrast for text on white background, please choose a "
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:294
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:306
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:295
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:307
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:588
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:595
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:41
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:646
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:42
|
||||
msgid "Others"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:70
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:71
|
||||
msgid "Count"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:120
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:121
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/subevent.js:108
|
||||
msgid "(one more date)"
|
||||
msgid_plural "({num} more dates)"
|
||||
@@ -270,87 +282,193 @@ msgid "plus %(rate)s% %(taxname)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:21
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "currently available: %s"
|
||||
msgid "incl. taxes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||
msgctxt "widget"
|
||||
msgid "Only available with a voucher"
|
||||
msgid "plus taxes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "minimum amount to order: %s"
|
||||
msgid "currently available: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||
msgctxt "widget"
|
||||
msgid "Close ticket shop"
|
||||
msgid "Only available with a voucher"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgid "minimum amount to order: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:26
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgid "Close ticket shop"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:27
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:29
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"You currently have an active cart for this event. If you select more "
|
||||
"products, they will be added to your existing cart."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:32
|
||||
msgctxt "widget"
|
||||
msgid "Resume checkout"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:31
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
|
||||
"ticketing powered by pretix</a>"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
msgctxt "widget"
|
||||
msgid "Redeem a voucher"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:34
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Redeem"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "Voucher code"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
msgctxt "widget"
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
msgctxt "widget"
|
||||
msgid "Continue"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
msgctxt "widget"
|
||||
msgid "See variations"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different event"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||
msgctxt "widget"
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:43
|
||||
msgctxt "widget"
|
||||
msgid "Next month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:46
|
||||
msgid "Mo"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgid "Tu"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgid "We"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgid "Th"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgid "Fr"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgid "Sa"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgid "Su"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgid "January"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
msgid "February"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgid "March"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgid "April"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgid "May"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgid "June"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgid "July"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgid "August"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgid "September"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
msgid "October"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "November"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "December"
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
|
||||
"POT-Creation-Date: 2019-03-22 14:50+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
@@ -99,7 +99,7 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:193
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:28
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:20
|
||||
msgid "Close message"
|
||||
msgstr ""
|
||||
|
||||
@@ -167,44 +167,56 @@ msgstr ""
|
||||
msgid "Generating messages …"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:43
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:55
|
||||
msgid "Unknown error."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:205
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:217
|
||||
msgid "Your color has great contrast and is very easy to read!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:209
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:221
|
||||
msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:213
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:225
|
||||
msgid ""
|
||||
"Your color has bad contrast for text on white background, please choose a "
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:294
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:306
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:295
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:307
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:588
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:595
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:41
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:646
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:42
|
||||
msgid "Others"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:70
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:71
|
||||
msgid "Count"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:120
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:121
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/subevent.js:108
|
||||
msgid "(one more date)"
|
||||
msgid_plural "({num} more dates)"
|
||||
@@ -263,87 +275,193 @@ msgid "plus %(rate)s% %(taxname)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:21
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "currently available: %s"
|
||||
msgid "incl. taxes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||
msgctxt "widget"
|
||||
msgid "Only available with a voucher"
|
||||
msgid "plus taxes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "minimum amount to order: %s"
|
||||
msgid "currently available: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||
msgctxt "widget"
|
||||
msgid "Close ticket shop"
|
||||
msgid "Only available with a voucher"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgid "minimum amount to order: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:26
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgid "Close ticket shop"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:27
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:29
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"You currently have an active cart for this event. If you select more "
|
||||
"products, they will be added to your existing cart."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:32
|
||||
msgctxt "widget"
|
||||
msgid "Resume checkout"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:31
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
|
||||
"ticketing powered by pretix</a>"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
msgctxt "widget"
|
||||
msgid "Redeem a voucher"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:34
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Redeem"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "Voucher code"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
msgctxt "widget"
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
msgctxt "widget"
|
||||
msgid "Continue"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
msgctxt "widget"
|
||||
msgid "See variations"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different event"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||
msgctxt "widget"
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:43
|
||||
msgctxt "widget"
|
||||
msgid "Next month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:46
|
||||
msgid "Mo"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgid "Tu"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgid "We"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgid "Th"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgid "Fr"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgid "Sa"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgid "Su"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgid "January"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
msgid "February"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgid "March"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgid "April"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgid "May"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgid "June"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgid "July"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgid "August"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgid "September"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
msgid "October"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "November"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "December"
|
||||
msgstr ""
|
||||
|
||||
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: 2019-02-20 16:52+0000\n"
|
||||
"POT-Creation-Date: 2019-03-22 14:50+0000\n"
|
||||
"PO-Revision-Date: 2018-04-24 14:22+0000\n"
|
||||
"Last-Translator: Pernille Thorsen <perth@aarhus.dk>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -115,7 +115,7 @@ msgstr ""
|
||||
"minut, så tjek din internetforbindelse, genindlæs siden og prøv igen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:193
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:28
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:20
|
||||
msgid "Close message"
|
||||
msgstr "Luk besked"
|
||||
|
||||
@@ -184,44 +184,58 @@ msgstr "Der er sket en fejl."
|
||||
msgid "Generating messages …"
|
||||
msgstr "Opretter beskeder …"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:43
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:55
|
||||
msgid "Unknown error."
|
||||
msgstr "Ukendt fejl."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:205
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:217
|
||||
msgid "Your color has great contrast and is very easy to read!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:209
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:221
|
||||
msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:213
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:225
|
||||
msgid ""
|
||||
"Your color has bad contrast for text on white background, please choose a "
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:294
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:306
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:295
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:307
|
||||
msgid "None"
|
||||
msgstr "Ingen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:588
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:595
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:41
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:646
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:42
|
||||
msgid "Others"
|
||||
msgstr "Andre"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:70
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:71
|
||||
msgid "Count"
|
||||
msgstr "Antal"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:120
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:121
|
||||
#, fuzzy
|
||||
#| msgid "None"
|
||||
msgid "No"
|
||||
msgstr "Ingen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/subevent.js:108
|
||||
msgid "(one more date)"
|
||||
msgid_plural "({num} more dates)"
|
||||
@@ -278,43 +292,53 @@ msgid "plus %(rate)s% %(taxname)s"
|
||||
msgstr "plus %(rate)s% %(taxname)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:21
|
||||
msgctxt "widget"
|
||||
msgid "incl. taxes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||
msgctxt "widget"
|
||||
msgid "plus taxes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "currently available: %s"
|
||||
msgstr "tilgængelig: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||
msgctxt "widget"
|
||||
msgid "Only available with a voucher"
|
||||
msgstr "Kun tilgængelig med en voucher"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "minimum amount to order: %s"
|
||||
msgstr "minimumsantal: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:26
|
||||
msgctxt "widget"
|
||||
msgid "Close ticket shop"
|
||||
msgstr "Luk billetbutik"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:27
|
||||
msgctxt "widget"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgstr "Billetbutikken kunne ikke hentes."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:26
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr "Kurven kunne ikke oprettes. Prøv igen senere"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:27
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:29
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgstr "Venteliste"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
#, fuzzy
|
||||
#| msgctxt "widget"
|
||||
#| msgid ""
|
||||
@@ -330,12 +354,12 @@ msgstr ""
|
||||
"varer vil de blive tilføjet din eksisterende kurv. Klik på denne besked for "
|
||||
"at gå mod kassen med din kurv."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:32
|
||||
msgctxt "widget"
|
||||
msgid "Resume checkout"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:31
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
|
||||
@@ -344,36 +368,132 @@ msgstr ""
|
||||
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener"
|
||||
"\">billetsystem drevet af pretix</a>"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
msgctxt "widget"
|
||||
msgid "Redeem a voucher"
|
||||
msgstr "Indløs voucher"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:34
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Redeem"
|
||||
msgstr "Indløs"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "Voucher code"
|
||||
msgstr "Voucherkode"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
msgctxt "widget"
|
||||
msgid "Close"
|
||||
msgstr "Luk"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
msgctxt "widget"
|
||||
msgid "Continue"
|
||||
msgstr "Fortsæt"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
msgctxt "widget"
|
||||
msgid "See variations"
|
||||
msgstr "Vis varianter"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different event"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||
msgctxt "widget"
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:43
|
||||
msgctxt "widget"
|
||||
msgid "Next month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:46
|
||||
msgid "Mo"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgid "Tu"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgid "We"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgid "Th"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgid "Fr"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgid "Sa"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgid "Su"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgid "January"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
msgid "February"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgid "March"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgid "April"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgid "May"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgid "June"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgid "July"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgid "August"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgid "September"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
msgid "October"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "November"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "December"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Your request has been queued on the server and will now be processed. If "
|
||||
#~ "this takes longer than two minutes, please contact us or go back in your "
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,9 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
|
||||
"PO-Revision-Date: 2018-12-02 15:44+0000\n"
|
||||
"Last-Translator: Alexander Schwartz <alexander.schwartz@gmx.net>\n"
|
||||
"POT-Creation-Date: 2019-03-22 14:50+0000\n"
|
||||
"PO-Revision-Date: 2019-03-22 15:00+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"de/>\n"
|
||||
"Language: de\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 3.1.1\n"
|
||||
"X-Generator: Weblate 3.5.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -114,7 +114,7 @@ msgstr ""
|
||||
"Seite neu laden und es erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:193
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:28
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:20
|
||||
msgid "Close message"
|
||||
msgstr "Schließen"
|
||||
|
||||
@@ -184,20 +184,20 @@ msgstr "Ein Fehler ist aufgetreten."
|
||||
msgid "Generating messages …"
|
||||
msgstr "Generiere Nachrichten…"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:43
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:55
|
||||
msgid "Unknown error."
|
||||
msgstr "Unbekannter Fehler."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:205
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:217
|
||||
msgid "Your color has great contrast and is very easy to read!"
|
||||
msgstr "Diese Farbe hat einen sehr guten Kontrast und ist sehr gut zu lesen!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:209
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:221
|
||||
msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||
msgstr ""
|
||||
"Diese Farbe hat einen ausreichenden Kontrast und wahrscheinlich gut zu lesen!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:213
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:225
|
||||
msgid ""
|
||||
"Your color has bad contrast for text on white background, please choose a "
|
||||
"darker shade."
|
||||
@@ -205,26 +205,38 @@ 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:294
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:306
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:295
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:307
|
||||
msgid "None"
|
||||
msgstr "Keine"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:588
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:595
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Intern einen anderen Namen verwenden"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:41
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:646
|
||||
msgid "Click to close"
|
||||
msgstr "Klicken zum Schließen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:42
|
||||
msgid "Others"
|
||||
msgstr "Sonstige"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:70
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:71
|
||||
msgid "Count"
|
||||
msgstr "Anzahl"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:120
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:121
|
||||
msgid "No"
|
||||
msgstr "Nein"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/subevent.js:108
|
||||
msgid "(one more date)"
|
||||
msgid_plural "({num} more dates)"
|
||||
@@ -283,43 +295,53 @@ msgid "plus %(rate)s% %(taxname)s"
|
||||
msgstr "zzgl. %(rate)s% %(taxname)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:21
|
||||
msgctxt "widget"
|
||||
msgid "incl. taxes"
|
||||
msgstr "inkl. Steuern"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||
msgctxt "widget"
|
||||
msgid "plus taxes"
|
||||
msgstr "zzgl. Steuern"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "currently available: %s"
|
||||
msgstr "aktuell verfügbar: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||
msgctxt "widget"
|
||||
msgid "Only available with a voucher"
|
||||
msgstr "Nur mit Gutschein verfügbar"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "minimum amount to order: %s"
|
||||
msgstr "minimale Bestellmenge: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:26
|
||||
msgctxt "widget"
|
||||
msgid "Close ticket shop"
|
||||
msgstr "Ticket-Shop schließen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:27
|
||||
msgctxt "widget"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgstr "Der Ticket-Shop konnte nicht geladen werden."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:26
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr "Der Warenkorb konnte nicht erstellt werden. Bitte erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:27
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:29
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgstr "Warteliste"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"You currently have an active cart for this event. If you select more "
|
||||
@@ -328,12 +350,12 @@ msgstr ""
|
||||
"Sie haben einen aktiven Warenkorb für diese Veranstaltung. Wenn Sie mehr "
|
||||
"Produkte auswählen, werden diese zu Ihrem Warenkorb hinzugefügt."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:32
|
||||
msgctxt "widget"
|
||||
msgid "Resume checkout"
|
||||
msgstr "Kauf fortsetzen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:31
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
|
||||
@@ -342,36 +364,138 @@ msgstr ""
|
||||
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">Event-"
|
||||
"Ticketshop von pretix</a>"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
msgctxt "widget"
|
||||
msgid "Redeem a voucher"
|
||||
msgstr "Gutschein einlösen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:34
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Redeem"
|
||||
msgstr "Einlösen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "Voucher code"
|
||||
msgstr "Gutscheincode"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
msgctxt "widget"
|
||||
msgid "Close"
|
||||
msgstr "Schließen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
msgctxt "widget"
|
||||
msgid "Continue"
|
||||
msgstr "Weiter"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
msgctxt "widget"
|
||||
msgid "See variations"
|
||||
msgstr "Varianten zeigen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different event"
|
||||
msgstr "Andere Veranstaltung auswählen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||
msgctxt "widget"
|
||||
msgid "Back"
|
||||
msgstr "Zurück"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:43
|
||||
msgctxt "widget"
|
||||
msgid "Next month"
|
||||
msgstr "Nächster Monat"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr "Vorheriger Monat"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:46
|
||||
msgid "Mo"
|
||||
msgstr "Mo"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgid "Tu"
|
||||
msgstr "Di"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgid "We"
|
||||
msgstr "Mi"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgid "Th"
|
||||
msgstr "Do"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgid "Fr"
|
||||
msgstr "Fr"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgid "Sa"
|
||||
msgstr "Sa"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgid "Su"
|
||||
msgstr "So"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgid "January"
|
||||
msgstr "Januar"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
msgid "February"
|
||||
msgstr "Februar"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgid "March"
|
||||
msgstr "März"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgid "April"
|
||||
msgstr "April"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgid "May"
|
||||
msgstr "Mai"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgid "June"
|
||||
msgstr "Juni"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgid "July"
|
||||
msgstr "Juli"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgid "August"
|
||||
msgstr "August"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgid "September"
|
||||
msgstr "September"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
msgid "October"
|
||||
msgstr "Oktober"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "November"
|
||||
msgstr "November"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "December"
|
||||
msgstr "Dezember"
|
||||
|
||||
#~ msgid "Ja"
|
||||
#~ msgstr "Ja"
|
||||
|
||||
#~ msgid "Nein"
|
||||
#~ msgstr "Nein"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Your request has been queued on the server and will now be processed. If "
|
||||
#~ "this takes longer than two minutes, please contact us or go back in your "
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,9 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-02-20 16:52+0000\n"
|
||||
"PO-Revision-Date: 2018-12-02 13:41+0000\n"
|
||||
"Last-Translator: Alexander Schwartz <alexander.schwartz@gmx.net>\n"
|
||||
"POT-Creation-Date: 2019-03-22 14:50+0000\n"
|
||||
"PO-Revision-Date: 2019-03-22 15:00+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix-js/de_Informal/>\n"
|
||||
"Language: de_Informal\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 3.1.1\n"
|
||||
"X-Generator: Weblate 3.5.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -113,7 +113,7 @@ msgstr ""
|
||||
"neu laden und es erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:193
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:28
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:20
|
||||
msgid "Close message"
|
||||
msgstr "Schließen"
|
||||
|
||||
@@ -183,20 +183,20 @@ msgstr "Ein Fehler ist aufgetreten."
|
||||
msgid "Generating messages …"
|
||||
msgstr "Generiere Nachrichten…"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:43
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:55
|
||||
msgid "Unknown error."
|
||||
msgstr "Unbekannter Fehler."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:205
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:217
|
||||
msgid "Your color has great contrast and is very easy to read!"
|
||||
msgstr "Diese Farbe hat einen sehr guten Kontrast und ist sehr gut zu lesen!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:209
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:221
|
||||
msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||
msgstr ""
|
||||
"Diese Farbe hat einen ausreichenden Kontrast und wahrscheinlich gut zu lesen!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:213
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:225
|
||||
msgid ""
|
||||
"Your color has bad contrast for text on white background, please choose a "
|
||||
"darker shade."
|
||||
@@ -204,26 +204,38 @@ 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:294
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:306
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:295
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:307
|
||||
msgid "None"
|
||||
msgstr "Keine"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:588
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:595
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Intern einen anderen Namen verwenden"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:41
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:646
|
||||
msgid "Click to close"
|
||||
msgstr "Klicken zum Schließen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:42
|
||||
msgid "Others"
|
||||
msgstr "Sonstige"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:70
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:71
|
||||
msgid "Count"
|
||||
msgstr "Anzahl"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:120
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:121
|
||||
msgid "No"
|
||||
msgstr "Nein"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/subevent.js:108
|
||||
msgid "(one more date)"
|
||||
msgid_plural "({num} more dates)"
|
||||
@@ -282,43 +294,53 @@ msgid "plus %(rate)s% %(taxname)s"
|
||||
msgstr "zzgl. %(rate)s% %(taxname)s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:21
|
||||
msgctxt "widget"
|
||||
msgid "incl. taxes"
|
||||
msgstr "inkl. Steuern"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||
msgctxt "widget"
|
||||
msgid "plus taxes"
|
||||
msgstr "zzgl. Steuern"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "currently available: %s"
|
||||
msgstr "aktuell verfügbar: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||
msgctxt "widget"
|
||||
msgid "Only available with a voucher"
|
||||
msgstr "Nur mit Gutschein verfügbar"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "minimum amount to order: %s"
|
||||
msgstr "minimale Bestellmenge: %s"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:26
|
||||
msgctxt "widget"
|
||||
msgid "Close ticket shop"
|
||||
msgstr "Ticket-Shop schließen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:27
|
||||
msgctxt "widget"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgstr "Der Ticket-Shop konnte nicht geladen werden."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:26
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr "Der Warenkorb konnte nicht erstellt werden. Bitte erneut versuchen."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:27
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:29
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgstr "Warteliste"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"You currently have an active cart for this event. If you select more "
|
||||
@@ -327,12 +349,12 @@ msgstr ""
|
||||
"Du hast einen aktiven Warenkorb für diese Veranstaltung. Wenn du mehr "
|
||||
"Produkte auswählst, werden diese zu deinem Warenkorb hinzugefügt."
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:32
|
||||
msgctxt "widget"
|
||||
msgid "Resume checkout"
|
||||
msgstr "Kauf fortsetzen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:31
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
|
||||
@@ -341,36 +363,138 @@ msgstr ""
|
||||
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">Event-"
|
||||
"Ticketshop von pretix</a>"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
msgctxt "widget"
|
||||
msgid "Redeem a voucher"
|
||||
msgstr "Gutschein einlösen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:34
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Redeem"
|
||||
msgstr "Einlösen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "Voucher code"
|
||||
msgstr "Gutscheincode"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
msgctxt "widget"
|
||||
msgid "Close"
|
||||
msgstr "Schließen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
msgctxt "widget"
|
||||
msgid "Continue"
|
||||
msgstr "Fortfahren"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
msgctxt "widget"
|
||||
msgid "See variations"
|
||||
msgstr "Varianten zeigen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different event"
|
||||
msgstr "Andere Veranstaltung auswählen"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||
msgctxt "widget"
|
||||
msgid "Back"
|
||||
msgstr "Zurück"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:43
|
||||
msgctxt "widget"
|
||||
msgid "Next month"
|
||||
msgstr "Nächster Monat"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr "Vorheriger Monat"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:46
|
||||
msgid "Mo"
|
||||
msgstr "Mo"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgid "Tu"
|
||||
msgstr "Di"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgid "We"
|
||||
msgstr "Mi"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgid "Th"
|
||||
msgstr "Do"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgid "Fr"
|
||||
msgstr "Fr"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgid "Sa"
|
||||
msgstr "Sa"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgid "Su"
|
||||
msgstr "So"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgid "January"
|
||||
msgstr "Januar"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
msgid "February"
|
||||
msgstr "Februar"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgid "March"
|
||||
msgstr "März"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgid "April"
|
||||
msgstr "April"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgid "May"
|
||||
msgstr "Mai"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgid "June"
|
||||
msgstr "Juni"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgid "July"
|
||||
msgstr "Juli"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgid "August"
|
||||
msgstr "August"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgid "September"
|
||||
msgstr "September"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
msgid "October"
|
||||
msgstr "Oktober"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "November"
|
||||
msgstr "November"
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "December"
|
||||
msgstr "Dezember"
|
||||
|
||||
#~ msgid "Ja"
|
||||
#~ msgstr "Ja"
|
||||
|
||||
#~ msgid "Nein"
|
||||
#~ msgstr "Nein"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Your request has been queued on the server and will now be processed. If "
|
||||
#~ "this takes longer than two minutes, please contact us or go back in your "
|
||||
|
||||
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: 2019-02-20 16:52+0000\n"
|
||||
"POT-Creation-Date: 2019-03-22 14:50+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"
|
||||
@@ -100,7 +100,7 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:193
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:28
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:20
|
||||
msgid "Close message"
|
||||
msgstr ""
|
||||
|
||||
@@ -168,44 +168,56 @@ msgstr ""
|
||||
msgid "Generating messages …"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:43
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:55
|
||||
msgid "Unknown error."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:205
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:217
|
||||
msgid "Your color has great contrast and is very easy to read!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:209
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:221
|
||||
msgid "Your color has decent contrast and is probably good-enough to read!"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:213
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:225
|
||||
msgid ""
|
||||
"Your color has bad contrast for text on white background, please choose a "
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:294
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:306
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:295
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:307
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:588
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:595
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:41
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:646
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:42
|
||||
msgid "Others"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:70
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:71
|
||||
msgid "Count"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:120
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:121
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/subevent.js:108
|
||||
msgid "(one more date)"
|
||||
msgid_plural "({num} more dates)"
|
||||
@@ -262,87 +274,193 @@ msgid "plus %(rate)s% %(taxname)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:21
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "currently available: %s"
|
||||
msgid "incl. taxes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:22
|
||||
msgctxt "widget"
|
||||
msgid "Only available with a voucher"
|
||||
msgid "plus taxes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:23
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "minimum amount to order: %s"
|
||||
msgid "currently available: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:24
|
||||
msgctxt "widget"
|
||||
msgid "Close ticket shop"
|
||||
msgid "Only available with a voucher"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:25
|
||||
#, javascript-format
|
||||
msgctxt "widget"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgid "minimum amount to order: %s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:26
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgid "Close ticket shop"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:27
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgid "The ticket shop could not be loaded."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:28
|
||||
msgctxt "widget"
|
||||
msgid "The cart could not be created. Please try again later"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:29
|
||||
msgctxt "widget"
|
||||
msgid "Waiting list"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"You currently have an active cart for this event. If you select more "
|
||||
"products, they will be added to your existing cart."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:30
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:32
|
||||
msgctxt "widget"
|
||||
msgid "Resume checkout"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:31
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
msgctxt "widget"
|
||||
msgid ""
|
||||
"<a href=\"https://pretix.eu\" target=\"_blank\" rel=\"noopener\">event "
|
||||
"ticketing powered by pretix</a>"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:33
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
msgctxt "widget"
|
||||
msgid "Redeem a voucher"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:34
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
msgctxt "widget"
|
||||
msgid "Redeem"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:35
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
msgctxt "widget"
|
||||
msgid "Voucher code"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:36
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
msgctxt "widget"
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:37
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:39
|
||||
msgctxt "widget"
|
||||
msgid "Continue"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:38
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:40
|
||||
msgctxt "widget"
|
||||
msgid "See variations"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:41
|
||||
msgctxt "widget"
|
||||
msgid "Choose a different event"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:42
|
||||
msgctxt "widget"
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:43
|
||||
msgctxt "widget"
|
||||
msgid "Next month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:44
|
||||
msgctxt "widget"
|
||||
msgid "Previous month"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:46
|
||||
msgid "Mo"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:47
|
||||
msgid "Tu"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:48
|
||||
msgid "We"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:49
|
||||
msgid "Th"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:50
|
||||
msgid "Fr"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:51
|
||||
msgid "Sa"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:52
|
||||
msgid "Su"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:55
|
||||
msgid "January"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:56
|
||||
msgid "February"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:57
|
||||
msgid "March"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:58
|
||||
msgid "April"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:59
|
||||
msgid "May"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:60
|
||||
msgid "June"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:61
|
||||
msgid "July"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:62
|
||||
msgid "August"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:63
|
||||
msgid "September"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:64
|
||||
msgid "October"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:65
|
||||
msgid "November"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixpresale/js/widget/widget.js:66
|
||||
msgid "December"
|
||||
msgstr ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user