forked from CGM_Public/pretix_original
Compare commits
218
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e1e082ee3 | ||
|
|
9a57371f9e | ||
|
|
8bc7045bba | ||
|
|
3c29223e5c | ||
|
|
35350a13d6 | ||
|
|
0d93f7f52f | ||
|
|
170dcf93e7 | ||
|
|
9319202213 | ||
|
|
bfd0eee2c1 | ||
|
|
8570f53ed0 | ||
|
|
f56f6dd628 | ||
|
|
413fabd821 | ||
|
|
9813e59210 | ||
|
|
d91d942eac | ||
|
|
22104f79bd | ||
|
|
f289ad9e4f | ||
|
|
f81a734716 | ||
|
|
7a27a42e79 | ||
|
|
65a2bab9bb | ||
|
|
a26f46b619 | ||
|
|
5c37c85415 | ||
|
|
8ddba36690 | ||
|
|
f9bf05e09b | ||
|
|
8471422bba | ||
|
|
ee9acebe03 | ||
|
|
35d2a73f75 | ||
|
|
eb3eca45b5 | ||
|
|
f7816924b0 | ||
|
|
12c3fef390 | ||
|
|
8e39aaa292 | ||
|
|
ee186b283d | ||
|
|
87130c3f2c | ||
|
|
23e2fda762 | ||
|
|
dc4e82905f | ||
|
|
6845771148 | ||
|
|
c26bec93c8 | ||
|
|
46238eb157 | ||
|
|
b3298c91c3 | ||
|
|
7801d06d17 | ||
|
|
9cc1d16676 | ||
|
|
8dd3ec89e0 | ||
|
|
7a419f9bb5 | ||
|
|
c594b6c1e5 | ||
|
|
763e811c7b | ||
|
|
380dc46deb | ||
|
|
82f6084059 | ||
|
|
9af889ad02 | ||
|
|
9869516b9c | ||
|
|
84180f5af4 | ||
|
|
cf781fc79e | ||
|
|
faa14a610c | ||
|
|
997deb72e1 | ||
|
|
d84998143e | ||
|
|
deef067dae | ||
|
|
8356c0f5bf | ||
|
|
042e60ee1c | ||
|
|
a3202ffc71 | ||
|
|
c8ef681cc3 | ||
|
|
63e4841460 | ||
|
|
af503d06fe | ||
|
|
ec24776e66 | ||
|
|
9a1163c65a | ||
|
|
1237b8ba47 | ||
|
|
364d86085c | ||
|
|
f7d52abb0e | ||
|
|
158b69ddb2 | ||
|
|
1e1af7572a | ||
|
|
fe05e11f6d | ||
|
|
94c1bd2e7e | ||
|
|
fa43fb702d | ||
|
|
70b466971c | ||
|
|
d86eef66fa | ||
|
|
f2ce4b8feb | ||
|
|
c9077a0e15 | ||
|
|
337406b612 | ||
|
|
cdcd1f5cc9 | ||
|
|
12eca31426 | ||
|
|
bda9813253 | ||
|
|
77e96564ec | ||
|
|
7fdbe1edd6 | ||
|
|
dc91d049ea | ||
|
|
7a11be63ec | ||
|
|
5eef82f616 | ||
|
|
c6b00e9ad6 | ||
|
|
6da9111519 | ||
|
|
8448e0e35c | ||
|
|
a3aa69d203 | ||
|
|
2a262c78d6 | ||
|
|
20202d3f50 | ||
|
|
cdd26dabaa | ||
|
|
5bbde9b53d | ||
|
|
b7e80a5a8d | ||
|
|
cc9fe68aa4 | ||
|
|
73ea04b4c0 | ||
|
|
edd9e1c9c1 | ||
|
|
3aec3c739f | ||
|
|
f336a0d259 | ||
|
|
9fb8657e00 | ||
|
|
9fdc6a5f16 | ||
|
|
6bdc7f8b41 | ||
|
|
e7cd5a3215 | ||
|
|
5400a3cdea | ||
|
|
c75c080c5c | ||
|
|
5eff9a86f4 | ||
|
|
0f8ac3ffb3 | ||
|
|
d6f0615712 | ||
|
|
e0524f2a03 | ||
|
|
db013f5e8c | ||
|
|
1c3623b223 | ||
|
|
c9007de853 | ||
|
|
a7052abb43 | ||
|
|
07b8555fa6 | ||
|
|
fb26229834 | ||
|
|
6a508b87f7 | ||
|
|
c9c379346e | ||
|
|
19fce8b086 | ||
|
|
20ad0becb3 | ||
|
|
e489134bdb | ||
|
|
9dc7201f50 | ||
|
|
376bb48686 | ||
|
|
8f85c015fb | ||
|
|
2decf026e9 | ||
|
|
02b42bd7ab | ||
|
|
78d8e49990 | ||
|
|
0de8239348 | ||
|
|
e644faf6b3 | ||
|
|
8d6d0c5893 | ||
|
|
37ba5a983b | ||
|
|
6fd8f9809c | ||
|
|
58dd6d7600 | ||
|
|
6d7e585d97 | ||
|
|
104c11d5dc | ||
|
|
90ee435f55 | ||
|
|
1d1f68945f | ||
|
|
6e4e161973 | ||
|
|
14fcacfb4d | ||
|
|
676b37f9c2 | ||
|
|
b81accf551 | ||
|
|
759d13f7b6 | ||
|
|
f73cb4cda3 | ||
|
|
8376a2da23 | ||
|
|
eeea64bd53 | ||
|
|
7caa957f07 | ||
|
|
5657ed8173 | ||
|
|
148addaaea | ||
|
|
22d2b23b37 | ||
|
|
959e940be7 | ||
|
|
0ddae0ed99 | ||
|
|
abf8c65d8b | ||
|
|
069c599cff | ||
|
|
e7d6bfd8b1 | ||
|
|
4678cef32a | ||
|
|
5de3b76718 | ||
|
|
670e22a611 | ||
|
|
c0419518c3 | ||
|
|
916ee0697f | ||
|
|
813a2332eb | ||
|
|
b4558201f5 | ||
|
|
059cc2ab09 | ||
|
|
e194063827 | ||
|
|
6ae5eecf27 | ||
|
|
89fe3d5bd2 | ||
|
|
0de58cd213 | ||
|
|
ddb9b3f445 | ||
|
|
96414d90d4 | ||
|
|
502cb60dc5 | ||
|
|
d680652aa8 | ||
|
|
0060f98233 | ||
|
|
aa3af8790c | ||
|
|
814c475475 | ||
|
|
61526f5465 | ||
|
|
19e762c9b9 | ||
|
|
1777a954a9 | ||
|
|
b8c7ace30e | ||
|
|
e153fa7227 | ||
|
|
232366a639 | ||
|
|
9afaa677c4 | ||
|
|
bb67ecc8e6 | ||
|
|
ce8bee5c11 | ||
|
|
abfca211b8 | ||
|
|
60a4428ebb | ||
|
|
4058772da8 | ||
|
|
55b4059abe | ||
|
|
497cb8de06 | ||
|
|
0897e375c8 | ||
|
|
0c81b57225 | ||
|
|
6fac1aeb62 | ||
|
|
996621c028 | ||
|
|
119a2621b5 | ||
|
|
85dd7a078e | ||
|
|
14114a6c1f | ||
|
|
f79ac05dcb | ||
|
|
5bacbfa9f1 | ||
|
|
c051d04462 | ||
|
|
1d0eb81659 | ||
|
|
f97effd0b7 | ||
|
|
4f71244e64 | ||
|
|
d800447cd6 | ||
|
|
b29686d9f2 | ||
|
|
369f4b6f8d | ||
|
|
11594346eb | ||
|
|
4dc5c770e3 | ||
|
|
f4de616e73 | ||
|
|
3b615fea6d | ||
|
|
dca0329cd1 | ||
|
|
0100383686 | ||
|
|
835788e477 | ||
|
|
298c8989f1 | ||
|
|
135dec81ff | ||
|
|
2a8b6ae66a | ||
|
|
e86eb345b3 | ||
|
|
050ff43a55 | ||
|
|
00a77f8652 | ||
|
|
6a2dc32c1d | ||
|
|
740dcceda7 | ||
|
|
3810dcd5b8 | ||
|
|
fa4cdbfe4a | ||
|
|
0e008812c3 |
@@ -0,0 +1,49 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
paths-ignore:
|
||||||
|
- 'doc/**'
|
||||||
|
- 'src/pretix/locale/**'
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
paths-ignore:
|
||||||
|
- 'doc/**'
|
||||||
|
- 'src/pretix/locale/**'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
name: Packaging
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.11"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pip
|
||||||
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pip-
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: sudo apt update && sudo apt install gettext unzip
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: pip3 install -U setuptools build pip check-manifest
|
||||||
|
- name: Run check-manifest
|
||||||
|
run: check-manifest
|
||||||
|
- name: Run build
|
||||||
|
run: python -m build
|
||||||
|
- name: Check files
|
||||||
|
run: unzip -l dist/pretix*whl | grep node_modules || exit 1
|
||||||
+1
-1
@@ -30,7 +30,7 @@ pypi:
|
|||||||
- make npminstall
|
- make npminstall
|
||||||
- cd ..
|
- cd ..
|
||||||
- check-manifest
|
- check-manifest
|
||||||
- python setup.py sdist bdist_wheel
|
- python -m build
|
||||||
- twine check dist/*
|
- twine check dist/*
|
||||||
- twine upload dist/*
|
- twine upload dist/*
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
+11
-13
@@ -41,18 +41,6 @@ RUN apt-get update && \
|
|||||||
ENV LC_ALL=C.UTF-8 \
|
ENV LC_ALL=C.UTF-8 \
|
||||||
DJANGO_SETTINGS_MODULE=production_settings
|
DJANGO_SETTINGS_MODULE=production_settings
|
||||||
|
|
||||||
# To copy only the requirements files needed to install from PIP
|
|
||||||
COPY src/setup.py /pretix/src/setup.py
|
|
||||||
RUN pip3 install -U \
|
|
||||||
pip \
|
|
||||||
setuptools \
|
|
||||||
wheel && \
|
|
||||||
cd /pretix/src && \
|
|
||||||
PRETIX_DOCKER_BUILD=TRUE pip3 install \
|
|
||||||
-e ".[memcached,mysql]" \
|
|
||||||
gunicorn django-extensions ipython && \
|
|
||||||
rm -rf ~/.cache/pip
|
|
||||||
|
|
||||||
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
|
COPY deployment/docker/pretix.bash /usr/local/bin/pretix
|
||||||
COPY deployment/docker/supervisord /etc/supervisord
|
COPY deployment/docker/supervisord /etc/supervisord
|
||||||
COPY deployment/docker/supervisord.all.conf /etc/supervisord.all.conf
|
COPY deployment/docker/supervisord.all.conf /etc/supervisord.all.conf
|
||||||
@@ -60,9 +48,19 @@ COPY deployment/docker/supervisord.web.conf /etc/supervisord.web.conf
|
|||||||
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
|
COPY deployment/docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
COPY deployment/docker/nginx-max-body-size.conf /etc/nginx/conf.d/nginx-max-body-size.conf
|
COPY deployment/docker/nginx-max-body-size.conf /etc/nginx/conf.d/nginx-max-body-size.conf
|
||||||
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
|
COPY deployment/docker/production_settings.py /pretix/src/production_settings.py
|
||||||
|
COPY pyproject.toml /pretix/pyproject.toml
|
||||||
|
COPY _build /pretix/_build
|
||||||
COPY src /pretix/src
|
COPY src /pretix/src
|
||||||
|
|
||||||
RUN cd /pretix/src && python setup.py install
|
RUN pip3 install -U \
|
||||||
|
pip \
|
||||||
|
setuptools \
|
||||||
|
wheel && \
|
||||||
|
cd /pretix && \
|
||||||
|
PRETIX_DOCKER_BUILD=TRUE pip3 install \
|
||||||
|
-e ".[memcached,mysql]" \
|
||||||
|
gunicorn django-extensions ipython && \
|
||||||
|
rm -rf ~/.cache/pip
|
||||||
|
|
||||||
RUN chmod +x /usr/local/bin/pretix && \
|
RUN chmod +x /usr/local/bin/pretix && \
|
||||||
rm /etc/nginx/sites-enabled/default && \
|
rm /etc/nginx/sites-enabled/default && \
|
||||||
|
|||||||
+10
-9
@@ -1,6 +1,7 @@
|
|||||||
include LICENSE
|
include LICENSE
|
||||||
include README.rst
|
include README.rst
|
||||||
include src/Makefile
|
include src/Makefile
|
||||||
|
include _build/backend.py
|
||||||
global-include *.proto
|
global-include *.proto
|
||||||
recursive-include src/pretix/static *
|
recursive-include src/pretix/static *
|
||||||
recursive-include src/pretix/static.dist *
|
recursive-include src/pretix/static.dist *
|
||||||
@@ -32,15 +33,15 @@ recursive-include src/pretix/plugins/returnurl/templates *
|
|||||||
recursive-include src/pretix/plugins/returnurl/static *
|
recursive-include src/pretix/plugins/returnurl/static *
|
||||||
recursive-include src/pretix/plugins/webcheckin/templates *
|
recursive-include src/pretix/plugins/webcheckin/templates *
|
||||||
recursive-include src/pretix/plugins/webcheckin/static *
|
recursive-include src/pretix/plugins/webcheckin/static *
|
||||||
recursive-include src *.cfg
|
recursive-include src *.cfg
|
||||||
recursive-include src *.csv
|
recursive-include src *.csv
|
||||||
recursive-include src *.gitkeep
|
recursive-include src *.gitkeep
|
||||||
recursive-include src *.jpg
|
recursive-include src *.jpg
|
||||||
recursive-include src *.json
|
recursive-include src *.json
|
||||||
recursive-include src *.py
|
recursive-include src *.py
|
||||||
recursive-include src *.svg
|
recursive-include src *.svg
|
||||||
recursive-include src *.txt
|
recursive-include src *.txt
|
||||||
recursive-include src Makefile
|
recursive-include src Makefile
|
||||||
|
|
||||||
recursive-exclude doc *
|
recursive-exclude doc *
|
||||||
recursive-exclude deployment *
|
recursive-exclude deployment *
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import tomli
|
||||||
|
from setuptools import build_meta as _orig
|
||||||
|
from setuptools.build_meta import *
|
||||||
|
|
||||||
|
|
||||||
|
def get_requires_for_build_wheel(config_settings=None):
|
||||||
|
with open("pyproject.toml", "rb") as f:
|
||||||
|
p = tomli.load(f)
|
||||||
|
return [
|
||||||
|
*_orig.get_requires_for_build_wheel(config_settings),
|
||||||
|
*p['project']['dependencies']
|
||||||
|
]
|
||||||
@@ -51,7 +51,7 @@ For our standard docker installation, create the database and user like this::
|
|||||||
# sudo -u postgres createuser -P pretix
|
# sudo -u postgres createuser -P pretix
|
||||||
# sudo -u postgres createdb -O pretix pretix
|
# sudo -u postgres createdb -O pretix pretix
|
||||||
|
|
||||||
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``::
|
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 listen on the Docker interface by changing the following line in ``/etc/postgresql/<version>/main/postgresql.conf``::
|
||||||
|
|
||||||
listen_addresses = 'localhost,172.17.0.1'
|
listen_addresses = 'localhost,172.17.0.1'
|
||||||
|
|
||||||
@@ -153,4 +153,89 @@ And you're done! After you've verified everything has been copied correctly, you
|
|||||||
|
|
||||||
.. note:: Don't forget to update your backup process to back up your PostgreSQL database instead of your MySQL database now.
|
.. note:: Don't forget to update your backup process to back up your PostgreSQL database instead of your MySQL database now.
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Peer authentication failed
|
||||||
|
""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
Sometimes you might see an error message like this::
|
||||||
|
|
||||||
|
django.db.utils.OperationalError: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: Peer authentication failed for user "pretix"
|
||||||
|
|
||||||
|
It is important to understand that PostgreSQL by default offers two types of authentication:
|
||||||
|
|
||||||
|
- **Peer authentication**, which works automatically based on the Linux user you are working as. This requires that
|
||||||
|
the connection is made through a local socket (empty ``host=`` in ``pretix.cfg``) and the name of the PostgreSQL user
|
||||||
|
and the Linux user are identical.
|
||||||
|
|
||||||
|
- Typically, you might run into this error if you accidentally execute ``python -m pretix`` commands as root instead
|
||||||
|
of the ``pretix`` user.
|
||||||
|
|
||||||
|
- **Password authentication**, which requires a username and password and works over network connections. To force
|
||||||
|
password authentication instead of peer authentication, set ``host=127.0.0.1`` in ``pretix.cfg``.
|
||||||
|
|
||||||
|
- You can alter the password on a PostgreSQL shell using the command ``ALTER USER pretix WITH PASSWORD '***';``.
|
||||||
|
When creating a user with the ``createuser`` command, pass option ``-P`` to set a new password.
|
||||||
|
|
||||||
|
- Even with password authentication, PostgreSQL by default only allows local connections. To allow remote connections,
|
||||||
|
you need to adjust both the ``listen_address`` configuration parameter as well as the ``pg_hba.conf`` file (see above
|
||||||
|
for an example with the docker networking setup).
|
||||||
|
|
||||||
|
Database error: relation does not exist
|
||||||
|
"""""""""""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
If you see an error like this::
|
||||||
|
|
||||||
|
2023-04-17T19:20:47.744023Z ERROR Database error 42P01: relation "public.pretix_foobar" does not exist
|
||||||
|
QUERY: ALTER TABLE public.pretix_foobar DROP CONSTRAINT IF EXISTS pretix_foobar_order_id_57e2cb41_fk_pretixbas CASCADE;
|
||||||
|
2023-04-17T19:20:47.744023Z FATAL Failed to create the schema, see above.
|
||||||
|
|
||||||
|
The reason is most likely that in the past, you installed a pretix plugin that you no longer have installed. However,
|
||||||
|
the database still contains tables of that plugin. If you want to keep the data, reinstall the plugin and re-run the
|
||||||
|
``migrate`` step from above. If you want to get rid of the data, manually drop the table mentioned in the error message
|
||||||
|
from your MySQL database::
|
||||||
|
|
||||||
|
# mysql -u root pretix
|
||||||
|
mysql> DROP TABLE pretix_foobar;
|
||||||
|
|
||||||
|
Then, retry. You might see a new error message with a new table, which you can handle the same way.
|
||||||
|
|
||||||
|
Cleaning out a failed attempt
|
||||||
|
"""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
You might want to clean your PostgreSQL database before you try again after an error. You can do so like this::
|
||||||
|
|
||||||
|
# sudo -u postgres psql pretix
|
||||||
|
pretix=# DROP SCHEMA public CASCADE;
|
||||||
|
pretix=# CREATE SCHEMA public;
|
||||||
|
pretix=# ALTER SCHEMA public OWNER TO pretix;
|
||||||
|
|
||||||
|
``pgloader`` crashes with heap exhaustion error
|
||||||
|
"""""""""""""""""""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
On some larger databases, we've seen ``pgloader`` crash with error messages similar to this::
|
||||||
|
|
||||||
|
Heap exhausted during garbage collection: 16 bytes available, 48 requested.
|
||||||
|
|
||||||
|
Or this::
|
||||||
|
|
||||||
|
2021-01-04T21:31:17.367000Z ERROR A SB-KERNEL::HEAP-EXHAUSTED-ERROR condition without bindings for heap statistics. (If
|
||||||
|
you did not expect to see this message, please report it.
|
||||||
|
2021-01-04T21:31:17.382000Z ERROR The value
|
||||||
|
NIL
|
||||||
|
is not of type
|
||||||
|
NUMBER
|
||||||
|
when binding SB-KERNEL::X
|
||||||
|
|
||||||
|
The ``pgloader`` version distributed for Debian and Ubuntu is compiled with the ``SBCL`` compiler. If compiled with
|
||||||
|
``CCL``, these bugs go away. Unfortunately, it is pretty hard to compile ``pgloader`` manually with ``CCL``. If you
|
||||||
|
run into this, we therefore recommend using the docker container provided by the ``pgloader`` maintainers::
|
||||||
|
|
||||||
|
sudo docker run --rm -v /tmp:/tmp --network host -it dimitri/pgloader:ccl.latest pgloader /tmp/pretix.load
|
||||||
|
|
||||||
|
As peer authentication is not available from inside the container, this requires you to use password-based authentication
|
||||||
|
in PostgreSQL (see above).
|
||||||
|
|
||||||
|
|
||||||
.. _PostgreSQL repositories: https://wiki.postgresql.org/wiki/Apt
|
.. _PostgreSQL repositories: https://wiki.postgresql.org/wiki/Apt
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ currency string Currency of the
|
|||||||
testmode boolean Whether this is a test gift card
|
testmode boolean Whether this is a test gift card
|
||||||
expires datetime Expiry date (or ``null``)
|
expires datetime Expiry date (or ``null``)
|
||||||
conditions string Special terms and conditions for this card (or ``null``)
|
conditions string Special terms and conditions for this card (or ``null``)
|
||||||
|
owner_ticket integer Internal ID of an order position that is the "owner" of
|
||||||
|
this gift card and can view all transactions. When setting
|
||||||
|
this field, you can also give the ``secret`` of an order
|
||||||
|
position.
|
||||||
|
issuer string Organizer slug of the organizer who created this gift
|
||||||
|
card and is responsible for it.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
The gift card transaction resource contains the following public fields:
|
The gift card transaction resource contains the following public fields:
|
||||||
@@ -35,8 +41,17 @@ value money (string) Transaction amo
|
|||||||
event string Event slug, if the gift card was used in the web shop (or ``null``)
|
event string Event slug, if the gift card was used in the web shop (or ``null``)
|
||||||
order string Order code, if the gift card was used in the web shop (or ``null``)
|
order string Order code, if the gift card was used in the web shop (or ``null``)
|
||||||
text string Custom text of the transaction (or ``null``)
|
text string Custom text of the transaction (or ``null``)
|
||||||
|
info object Additional data about the transaction (or ``null``)
|
||||||
|
acceptor string Organizer slug of the organizer who created this transaction
|
||||||
|
(can be ``null`` for all transactions performed before
|
||||||
|
this field was added.)
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionchanged:: 4.20
|
||||||
|
|
||||||
|
The ``owner_ticket`` and ``issuer`` attributes of the gift card and the ``info`` and ``acceptor`` attributes of the
|
||||||
|
gift card transaction resource have been added.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -72,6 +87,8 @@ Endpoints
|
|||||||
"testmode": false,
|
"testmode": false,
|
||||||
"expires": null,
|
"expires": null,
|
||||||
"conditions": null,
|
"conditions": null,
|
||||||
|
"owner_ticket": null,
|
||||||
|
"issuer": "bigevents",
|
||||||
"value": "13.37"
|
"value": "13.37"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -81,6 +98,10 @@ Endpoints
|
|||||||
:query string secret: Only show gift cards with the given secret.
|
:query string secret: Only show gift cards with the given secret.
|
||||||
:query boolean testmode: Filter for gift cards that are (not) in test mode.
|
:query boolean testmode: Filter for gift cards that are (not) in test mode.
|
||||||
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||||
|
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
|
||||||
|
The nested objects are identical to the respective resources, except that the ``owner_ticket``
|
||||||
|
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||||
|
matching easier. The parameter can be given multiple times.
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
@@ -113,6 +134,8 @@ Endpoints
|
|||||||
"testmode": false,
|
"testmode": false,
|
||||||
"expires": null,
|
"expires": null,
|
||||||
"conditions": null,
|
"conditions": null,
|
||||||
|
"owner_ticket": null,
|
||||||
|
"issuer": "bigevents",
|
||||||
"value": "13.37"
|
"value": "13.37"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,10 +180,16 @@ Endpoints
|
|||||||
"currency": "EUR",
|
"currency": "EUR",
|
||||||
"expires": null,
|
"expires": null,
|
||||||
"conditions": null,
|
"conditions": null,
|
||||||
|
"owner_ticket": null,
|
||||||
|
"issuer": "bigevents",
|
||||||
"value": "13.37"
|
"value": "13.37"
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to create a gift card for
|
:param organizer: The ``slug`` field of the organizer to create a gift card for
|
||||||
|
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
|
||||||
|
The nested objects are identical to the respective resources, except that the ``owner_ticket``
|
||||||
|
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||||
|
matching easier. The parameter can be given multiple times.
|
||||||
:statuscode 201: no error
|
:statuscode 201: no error
|
||||||
:statuscode 400: The gift card could not be created due to invalid submitted data.
|
:statuscode 400: The gift card could not be created due to invalid submitted data.
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
@@ -205,6 +234,8 @@ Endpoints
|
|||||||
"currency": "EUR",
|
"currency": "EUR",
|
||||||
"expires": null,
|
"expires": null,
|
||||||
"conditions": null,
|
"conditions": null,
|
||||||
|
"owner_ticket": null,
|
||||||
|
"issuer": "bigevents",
|
||||||
"value": "14.00"
|
"value": "14.00"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +281,8 @@ Endpoints
|
|||||||
"testmode": false,
|
"testmode": false,
|
||||||
"expires": null,
|
"expires": null,
|
||||||
"conditions": null,
|
"conditions": null,
|
||||||
|
"owner_ticket": null,
|
||||||
|
"issuer": "bigevents",
|
||||||
"value": "15.37"
|
"value": "15.37"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +326,11 @@ Endpoints
|
|||||||
"value": "50.00",
|
"value": "50.00",
|
||||||
"event": "democon",
|
"event": "democon",
|
||||||
"order": "FXQYW",
|
"order": "FXQYW",
|
||||||
"text": null
|
"text": null,
|
||||||
|
"acceptor": "bigevents",
|
||||||
|
"info": {
|
||||||
|
"created_by": "plugin1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ at :ref:`plugin-docs`.
|
|||||||
item_variations
|
item_variations
|
||||||
item_bundles
|
item_bundles
|
||||||
item_add-ons
|
item_add-ons
|
||||||
|
item_meta_properties
|
||||||
questions
|
questions
|
||||||
question_options
|
question_options
|
||||||
quotas
|
quotas
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
Item Meta Properties
|
||||||
|
====================
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
An Item Meta Property is used to include (event internally relevant) meta information with every item (product). This
|
||||||
|
could be internal categories like booking positions.
|
||||||
|
|
||||||
|
The Item Meta Properties resource contains the following public fields:
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Unique ID for this property
|
||||||
|
name string Name of the property
|
||||||
|
default string Value of the default option
|
||||||
|
required boolean If ``true``, this property will have to be assigned a
|
||||||
|
value in all items of the related event
|
||||||
|
allowed_values list List of all permitted values for this property,
|
||||||
|
or ``null`` for no limitation
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/
|
||||||
|
|
||||||
|
Returns a list of all Item Meta Properties within a given event.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Color",
|
||||||
|
"default": "red",
|
||||||
|
"required": true,
|
||||||
|
"allowed_values": ["red", "green", "blue"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer
|
||||||
|
:param event: The ``slug`` field of the event
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/(id)/
|
||||||
|
|
||||||
|
Returns information on one property, identified by its id.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/1/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Color",
|
||||||
|
"default": "red",
|
||||||
|
"required": true,
|
||||||
|
"allowed_values": ["red", "green", "blue"]
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer
|
||||||
|
:param event: The ``slug`` field of the event
|
||||||
|
:param id: The ``id`` field of the item meta property to retrieve
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/
|
||||||
|
|
||||||
|
Creates a new item meta property
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "ref-code",
|
||||||
|
"default": "abcde",
|
||||||
|
"required": true,
|
||||||
|
"allowed_values": null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "ref-code",
|
||||||
|
"default": "abcde",
|
||||||
|
"required": true,
|
||||||
|
"allowed_values": null
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer
|
||||||
|
:param event: The ``slug`` field of the event
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 400: The item meta property 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:patch:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/(id)/
|
||||||
|
|
||||||
|
Update an item meta property. 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/item_meta_properties/2/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 94
|
||||||
|
|
||||||
|
{
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "ref-code",
|
||||||
|
"default": "abcde",
|
||||||
|
"required": false,
|
||||||
|
"allowed_values": []
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer
|
||||||
|
:param event: The ``slug`` field of the event
|
||||||
|
:param id: The ``id`` field of the item meta property to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The property could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/item_meta_properties/(id)/
|
||||||
|
|
||||||
|
Delete an item meta property.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /api/v1/organizers/bigevents/events/sampleconf/item_meta_properties/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
|
||||||
|
:param event: The ``slug`` field of the event
|
||||||
|
:param id: The ``id`` field of the item meta property to delete
|
||||||
|
:statuscode 204: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
|
||||||
@@ -91,11 +91,11 @@ Endpoints
|
|||||||
:query string updated_since: Only show media updated since a given date.
|
:query string updated_since: Only show media updated since a given date.
|
||||||
:query integer linked_orderposition: Only show media linked to the given ticket.
|
:query integer linked_orderposition: Only show media linked to the given ticket.
|
||||||
:query integer linked_giftcard: Only show media linked to the given gift card.
|
:query integer linked_giftcard: Only show media linked to the given gift card.
|
||||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
|
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
|
||||||
field will be shown as a nested value instead of just an ID. The nested objects are identical to
|
or ``"customer"``, the respective field will be shown as a nested value instead of just an ID.
|
||||||
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
|
The nested objects are identical to the respective resources, except that order positions
|
||||||
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
|
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||||
can be given multiple times.
|
matching easier. The parameter can be given multiple times.
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
@@ -138,11 +138,11 @@ Endpoints
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param id: The ``id`` field of the medium to fetch
|
:param id: The ``id`` field of the medium to fetch
|
||||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
|
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
|
||||||
field will be shown as a nested value instead of just an ID. The nested objects are identical to
|
or ``"customer"``, the respective field will be shown as a nested value instead of just an ID.
|
||||||
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
|
The nested objects are identical to the respective resources, except that order positions
|
||||||
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
|
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||||
can be given multiple times.
|
matching easier. The parameter can be given multiple times.
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ tag string A string that i
|
|||||||
comment string An internal comment on the voucher
|
comment string An internal comment on the voucher
|
||||||
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
|
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
|
||||||
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
||||||
|
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
|
||||||
|
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -95,6 +97,9 @@ Endpoints
|
|||||||
"comment": "",
|
"comment": "",
|
||||||
"seat": null,
|
"seat": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -161,7 +166,10 @@ Endpoints
|
|||||||
"tag": "testvoucher",
|
"tag": "testvoucher",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"seat": null,
|
"seat": null,
|
||||||
"subevent": null
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -198,7 +206,10 @@ Endpoints
|
|||||||
"quota": null,
|
"quota": null,
|
||||||
"tag": "testvoucher",
|
"tag": "testvoucher",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"subevent": null
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -225,7 +236,10 @@ Endpoints
|
|||||||
"tag": "testvoucher",
|
"tag": "testvoucher",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"seat": null,
|
"seat": null,
|
||||||
"subevent": null
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to create a voucher for
|
:param organizer: The ``slug`` field of the organizer to create a voucher for
|
||||||
@@ -264,7 +278,10 @@ Endpoints
|
|||||||
"quota": null,
|
"quota": null,
|
||||||
"tag": "testvoucher",
|
"tag": "testvoucher",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"subevent": null
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "ASDKLJCYXCASDASD",
|
"code": "ASDKLJCYXCASDASD",
|
||||||
@@ -279,7 +296,10 @@ Endpoints
|
|||||||
"quota": null,
|
"quota": null,
|
||||||
"tag": "testvoucher",
|
"tag": "testvoucher",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"subevent": null
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
},
|
},
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -353,7 +373,10 @@ Endpoints
|
|||||||
"tag": "testvoucher",
|
"tag": "testvoucher",
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"seat": null,
|
"seat": null,
|
||||||
"subevent": null
|
"subevent": null,
|
||||||
|
"show_hidden_items": false,
|
||||||
|
"all_addons_included": false,
|
||||||
|
"all_bundles_included": false
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Core
|
|||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
|
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types,
|
||||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
|
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
|
||||||
register_ticket_secret_generators
|
register_ticket_secret_generators, gift_card_transaction_display
|
||||||
|
|
||||||
Order events
|
Order events
|
||||||
""""""""""""
|
""""""""""""
|
||||||
@@ -21,7 +21,7 @@ Order events
|
|||||||
There are multiple signals that will be sent out in the ordering cycle:
|
There are multiple signals that will be sent out in the ordering cycle:
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: validate_cart, validate_cart_addons, validate_order, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
||||||
|
|
||||||
Check-ins
|
Check-ins
|
||||||
"""""""""
|
"""""""""
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
sphinx==6.2.*
|
sphinx==7.0.*
|
||||||
jinja2==3.1.*
|
jinja2==3.1.*
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-e ../
|
-e ../
|
||||||
sphinx==6.2.*
|
sphinx==7.0.*
|
||||||
jinja2==3.1.*
|
jinja2==3.1.*
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-httpdomain
|
||||||
|
|||||||
@@ -201,6 +201,10 @@ record for the subdomain ``pretix._domainkey`` with the following contents::
|
|||||||
|
|
||||||
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
|
Then, please contact support@pretix.eu and we will enable DKIM for your domain on our mail servers.
|
||||||
|
|
||||||
|
.. note:: Many SMTP servers impose rate limits on the sent emails, such as a maximum number of emails sent per hour.
|
||||||
|
These SMTP servers are often not suitable for use with pretix, in case you want to send an email to many
|
||||||
|
hundreds or thousands of ticket buyers. Depending on how the rate limit is implemented, emails might be lost
|
||||||
|
in this case, as pretix only retries email delivery for a certain time period.
|
||||||
|
|
||||||
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
|
.. _Sender Policy Framework: https://en.wikipedia.org/wiki/Sender_Policy_Framework
|
||||||
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax
|
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax
|
||||||
|
|||||||
+16
-39
@@ -26,7 +26,6 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
# Note that many of these are repeated as build-time dependencies down below -- change them too in case of updates!
|
|
||||||
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
||||||
"babel",
|
"babel",
|
||||||
"BeautifulSoup4==4.12.*",
|
"BeautifulSoup4==4.12.*",
|
||||||
@@ -41,9 +40,9 @@ dependencies = [
|
|||||||
"django-bootstrap3==23.1.*",
|
"django-bootstrap3==23.1.*",
|
||||||
"django-compressor==4.3.*",
|
"django-compressor==4.3.*",
|
||||||
"django-countries==7.5.*",
|
"django-countries==7.5.*",
|
||||||
"django-filter==23.1",
|
"django-filter==23.2",
|
||||||
"django-formset-js-improved==0.5.0.3",
|
"django-formset-js-improved==0.5.0.3",
|
||||||
"django-formtools==2.4",
|
"django-formtools==2.4.1",
|
||||||
"django-hierarkey==1.1.*",
|
"django-hierarkey==1.1.*",
|
||||||
"django-hijack==3.3.*",
|
"django-hijack==3.3.*",
|
||||||
"django-i18nfield==1.9.*,>=1.9.4",
|
"django-i18nfield==1.9.*,>=1.9.4",
|
||||||
@@ -52,17 +51,16 @@ dependencies = [
|
|||||||
"django-markup",
|
"django-markup",
|
||||||
"django-mysql",
|
"django-mysql",
|
||||||
"django-oauth-toolkit==2.2.*",
|
"django-oauth-toolkit==2.2.*",
|
||||||
"django-otp==1.1.*",
|
"django-otp==1.2.*",
|
||||||
"django-phonenumber-field==7.0.*",
|
"django-phonenumber-field==7.1.*",
|
||||||
"django-redis==5.2.*",
|
"django-redis==5.2.*",
|
||||||
"django-scopes==1.2.*",
|
"django-scopes==2.0.*",
|
||||||
"django-statici18n==2.3.*",
|
"django-statici18n==2.3.*",
|
||||||
"djangorestframework==3.14.*",
|
"djangorestframework==3.14.*",
|
||||||
"dnspython==2.2.*",
|
"dnspython==2.3.*",
|
||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
"geoip2==4.*",
|
"geoip2==4.*",
|
||||||
"importlib_metadata==6.6.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
"importlib_metadata==6.6.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||||
"idna",
|
|
||||||
"isoweek",
|
"isoweek",
|
||||||
"jsonschema",
|
"jsonschema",
|
||||||
"kombu==5.2.*",
|
"kombu==5.2.*",
|
||||||
@@ -70,7 +68,7 @@ dependencies = [
|
|||||||
"lxml",
|
"lxml",
|
||||||
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
"markdown==3.4.3", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||||
"mt-940==4.23.*",
|
"mt-940==4.30.*",
|
||||||
"oauthlib==3.2.*",
|
"oauthlib==3.2.*",
|
||||||
"openpyxl==3.1.*",
|
"openpyxl==3.1.*",
|
||||||
"packaging",
|
"packaging",
|
||||||
@@ -79,12 +77,12 @@ dependencies = [
|
|||||||
"PyJWT==2.6.*",
|
"PyJWT==2.6.*",
|
||||||
"phonenumberslite==8.13.*",
|
"phonenumberslite==8.13.*",
|
||||||
"Pillow==9.5.*",
|
"Pillow==9.5.*",
|
||||||
"protobuf==4.22.*",
|
"protobuf==4.23.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.21",
|
"pycparser==2.21",
|
||||||
"pycryptodome==3.17.*",
|
"pycryptodome==3.18.*",
|
||||||
"pypdf==3.8.*",
|
"pypdf==3.9.*",
|
||||||
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
||||||
"python-dateutil==2.8.*",
|
"python-dateutil==2.8.*",
|
||||||
"python-u2flib-server==4.*",
|
"python-u2flib-server==4.*",
|
||||||
@@ -92,8 +90,8 @@ dependencies = [
|
|||||||
"pyuca",
|
"pyuca",
|
||||||
"qrcode==7.4.*",
|
"qrcode==7.4.*",
|
||||||
"redis==4.5.*,>=4.5.4",
|
"redis==4.5.*,>=4.5.4",
|
||||||
"reportlab==3.6.*",
|
"reportlab==4.0.*",
|
||||||
"requests==2.28.*",
|
"requests==2.30.*",
|
||||||
"sentry-sdk==1.15.*",
|
"sentry-sdk==1.15.*",
|
||||||
"sepaxml==2.6.*",
|
"sepaxml==2.6.*",
|
||||||
"slimit",
|
"slimit",
|
||||||
@@ -114,13 +112,9 @@ mysql = ["mysqlclient"]
|
|||||||
dev = [
|
dev = [
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"django-debug-toolbar==4.0.*",
|
|
||||||
"django-formset-js-improved==0.5.0.3",
|
|
||||||
"django-oauth-toolkit==2.2.*",
|
|
||||||
"flake8==6.0.*",
|
"flake8==6.0.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==5.12.*",
|
"isort==5.12.*",
|
||||||
"oauthlib==3.2.*",
|
|
||||||
"pep8-naming==0.13.*",
|
"pep8-naming==0.13.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pycodestyle==2.10.*",
|
"pycodestyle==2.10.*",
|
||||||
@@ -141,31 +135,14 @@ build = "pretix._build:CustomBuild"
|
|||||||
build_ext = "pretix._build:CustomBuildExt"
|
build_ext = "pretix._build:CustomBuildExt"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
build-backend = "backend"
|
||||||
|
backend-path = ["_build"]
|
||||||
requires = [
|
requires = [
|
||||||
"setuptools",
|
"setuptools",
|
||||||
"setuptools-rust",
|
"setuptools-rust",
|
||||||
"wheel",
|
"wheel",
|
||||||
"importlib_metadata",
|
"importlib_metadata",
|
||||||
|
"tomli",
|
||||||
# These are runtime dependencies that we unfortunately need to be import in the step that generates
|
|
||||||
# all CSS and JS asset files. We should keep their versions in sync with the definition above.
|
|
||||||
"babel",
|
|
||||||
"Django==3.2.*,>=3.2.18",
|
|
||||||
"django-bootstrap3==23.1.*",
|
|
||||||
"django-compressor==4.3.*",
|
|
||||||
"django-countries==7.5.*",
|
|
||||||
"django-formtools==2.4",
|
|
||||||
"django-hierarkey==1.1.*",
|
|
||||||
"django-i18nfield==1.9.*,>=1.9.4",
|
|
||||||
"django-libsass==0.9",
|
|
||||||
"django-phonenumber-field==7.0.*",
|
|
||||||
"django-statici18n==2.3.*",
|
|
||||||
"djangorestframework==3.14.*",
|
|
||||||
"libsass==0.22.*",
|
|
||||||
"phonenumberslite==8.13.*",
|
|
||||||
"pycountry",
|
|
||||||
"pyuca",
|
|
||||||
"slimit",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -183,4 +160,4 @@ version = {attr = "pretix.__version__"}
|
|||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
include = ["pretix*"]
|
include = ["pretix*"]
|
||||||
namespaces = false
|
namespaces = false
|
||||||
|
|||||||
@@ -19,8 +19,32 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import setuptools
|
import setuptools
|
||||||
|
|
||||||
|
|
||||||
|
sys.path.append(str(Path.cwd() / 'src'))
|
||||||
|
|
||||||
|
|
||||||
|
def _CustomBuild(*args, **kwargs):
|
||||||
|
print(sys.path)
|
||||||
|
from pretix._build import CustomBuild
|
||||||
|
return CustomBuild(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _CustomBuildExt(*args, **kwargs):
|
||||||
|
from pretix._build import CustomBuildExt
|
||||||
|
return CustomBuildExt(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
cmdclass = {
|
||||||
|
'build': _CustomBuild,
|
||||||
|
'build_ext': _CustomBuildExt,
|
||||||
|
}
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
setuptools.setup()
|
setuptools.setup(
|
||||||
|
cmdclass=cmdclass,
|
||||||
|
)
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "4.20.0.dev0"
|
__version__ = "4.21.0.dev0"
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ ALL_LANGUAGES = [
|
|||||||
('de-informal', _('German (informal)')),
|
('de-informal', _('German (informal)')),
|
||||||
('ar', _('Arabic')),
|
('ar', _('Arabic')),
|
||||||
('zh-hans', _('Chinese (simplified)')),
|
('zh-hans', _('Chinese (simplified)')),
|
||||||
|
('zh-hant', _('Chinese (traditional)')),
|
||||||
('cs', _('Czech')),
|
('cs', _('Czech')),
|
||||||
('da', _('Danish')),
|
('da', _('Danish')),
|
||||||
('nl', _('Dutch')),
|
('nl', _('Dutch')),
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
|||||||
('DELETE', 'api-v1:cartposition-detail'),
|
('DELETE', 'api-v1:cartposition-detail'),
|
||||||
('GET', 'api-v1:giftcard-list'),
|
('GET', 'api-v1:giftcard-list'),
|
||||||
('POST', 'api-v1:giftcard-transact'),
|
('POST', 'api-v1:giftcard-transact'),
|
||||||
|
('PATCH', 'api-v1:giftcard-detail'),
|
||||||
('GET', 'plugins:pretix_posbackend:posclosing-list'),
|
('GET', 'plugins:pretix_posbackend:posclosing-list'),
|
||||||
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
|
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
|
||||||
('POST', 'plugins:pretix_posbackend:posclosing-list'),
|
('POST', 'plugins:pretix_posbackend:posclosing-list'),
|
||||||
|
|||||||
@@ -19,3 +19,30 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class AsymmetricField(serializers.Field):
|
||||||
|
def __init__(self, read, write, **kwargs):
|
||||||
|
self.read = read
|
||||||
|
self.write = write
|
||||||
|
super().__init__(
|
||||||
|
required=self.write.required,
|
||||||
|
default=self.write.default,
|
||||||
|
initial=self.write.initial,
|
||||||
|
source=self.write.source if self.write.source != self.write.field_name else None,
|
||||||
|
label=self.write.label,
|
||||||
|
allow_null=self.write.allow_null,
|
||||||
|
error_messages=self.write.error_messages,
|
||||||
|
validators=self.write.validators,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return self.write.to_internal_value(data)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return self.read.to_representation(value)
|
||||||
|
|
||||||
|
def run_validation(self, data=serializers.empty):
|
||||||
|
return self.write.run_validation(data)
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
|||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
from pretix.base.models.items import (
|
||||||
|
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||||
|
)
|
||||||
from pretix.base.services.seating import (
|
from pretix.base.services.seating import (
|
||||||
SeatProtected, generate_seats, validate_plan_change,
|
SeatProtected, generate_seats, validate_plan_change,
|
||||||
)
|
)
|
||||||
@@ -683,6 +685,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'waiting_list_phones_asked',
|
'waiting_list_phones_asked',
|
||||||
'waiting_list_phones_required',
|
'waiting_list_phones_required',
|
||||||
'waiting_list_phones_explanation_text',
|
'waiting_list_phones_explanation_text',
|
||||||
|
'waiting_list_limit_per_user',
|
||||||
'max_items_per_order',
|
'max_items_per_order',
|
||||||
'reservation_time',
|
'reservation_time',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
@@ -765,6 +768,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'invoice_footer_text',
|
'invoice_footer_text',
|
||||||
'invoice_eu_currencies',
|
'invoice_eu_currencies',
|
||||||
'invoice_logo_image',
|
'invoice_logo_image',
|
||||||
|
'invoice_renderer_highlight_order_code',
|
||||||
'cancel_allow_user',
|
'cancel_allow_user',
|
||||||
'cancel_allow_user_until',
|
'cancel_allow_user_until',
|
||||||
'cancel_allow_user_unpaid_keep',
|
'cancel_allow_user_unpaid_keep',
|
||||||
@@ -902,3 +906,23 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
|||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiLineStringField(serializers.Field):
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return [v.strip() for v in value.splitlines()]
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if isinstance(data, list) and len(data) > 0:
|
||||||
|
return "\n".join(data)
|
||||||
|
else:
|
||||||
|
raise ValidationError('Invalid data type.')
|
||||||
|
|
||||||
|
|
||||||
|
class ItemMetaPropertiesSerializer(I18nAwareModelSerializer):
|
||||||
|
allowed_values = MultiLineStringField(allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ItemMetaProperty
|
||||||
|
fields = ('id', 'name', 'default', 'required', 'allowed_values')
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
|
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
|
||||||
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True)
|
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
|
||||||
|
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
|
||||||
|
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
|
||||||
else:
|
else:
|
||||||
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
|
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
|
||||||
required=False,
|
required=False,
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
|||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
if 'order' in self.context:
|
||||||
|
data['order'] = self.context['order']
|
||||||
if data.get('addon_to'):
|
if data.get('addon_to'):
|
||||||
try:
|
try:
|
||||||
data['addon_to'] = data['order'].positions.get(positionid=data['addon_to'])
|
data['addon_to'] = data['order'].positions.get(positionid=data['addon_to'])
|
||||||
|
|||||||
@@ -22,12 +22,14 @@
|
|||||||
import logging
|
import logging
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
from pretix.api.serializers import AsymmetricField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import CompatibleJSONField
|
from pretix.api.serializers.order import CompatibleJSONField
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
@@ -35,8 +37,8 @@ from pretix.base.auth import get_auth_backends
|
|||||||
from pretix.base.i18n import get_language_without_region
|
from pretix.base.i18n import get_language_without_region
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||||
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
MembershipType, OrderPosition, Organizer, ReusableMedium, SeatingPlan,
|
||||||
User,
|
Team, TeamAPIToken, TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||||
from pretix.base.services.mail import SendMailException, mail
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
@@ -127,8 +129,52 @@ class MembershipSerializer(I18nAwareModelSerializer):
|
|||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
|
||||||
|
if isinstance(data, int):
|
||||||
|
try:
|
||||||
|
return queryset.get(pk=data)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
self.fail('does_not_exist', pk_value=data)
|
||||||
|
|
||||||
|
elif isinstance(data, str):
|
||||||
|
try:
|
||||||
|
return queryset.get(
|
||||||
|
Q(secret=data)
|
||||||
|
| Q(pseudonymization_id=data)
|
||||||
|
| Q(pk__in=ReusableMedium.objects.filter(
|
||||||
|
organizer=self.context['organizer'],
|
||||||
|
type='barcode',
|
||||||
|
identifier=data
|
||||||
|
))
|
||||||
|
)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
self.fail('does_not_exist', pk_value=data)
|
||||||
|
|
||||||
|
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||||
|
|
||||||
|
|
||||||
class GiftCardSerializer(I18nAwareModelSerializer):
|
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||||
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
||||||
|
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
|
||||||
|
issuer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['owner_ticket'].queryset = OrderPosition.objects.filter(order__event__organizer=self.context['organizer'])
|
||||||
|
|
||||||
|
if 'owner_ticket' in self.context['request'].query_params.getlist('expand'):
|
||||||
|
from pretix.api.serializers.media import (
|
||||||
|
NestedOrderPositionSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields['owner_ticket'] = AsymmetricField(
|
||||||
|
NestedOrderPositionSerializer(read_only=True, context=self.context),
|
||||||
|
self.fields['owner_ticket'],
|
||||||
|
)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
@@ -151,7 +197,8 @@ class GiftCardSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GiftCard
|
model = GiftCard
|
||||||
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions')
|
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',
|
||||||
|
'issuer')
|
||||||
|
|
||||||
|
|
||||||
class OrderEventSlugField(serializers.RelatedField):
|
class OrderEventSlugField(serializers.RelatedField):
|
||||||
@@ -162,11 +209,12 @@ class OrderEventSlugField(serializers.RelatedField):
|
|||||||
|
|
||||||
class GiftCardTransactionSerializer(I18nAwareModelSerializer):
|
class GiftCardTransactionSerializer(I18nAwareModelSerializer):
|
||||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||||
|
acceptor = serializers.SlugRelatedField(slug_field='slug', read_only=True)
|
||||||
event = OrderEventSlugField(source='order', read_only=True)
|
event = OrderEventSlugField(source='order', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GiftCardTransaction
|
model = GiftCardTransaction
|
||||||
fields = ('id', 'datetime', 'value', 'event', 'order', 'text')
|
fields = ('id', 'datetime', 'value', 'event', 'order', 'text', 'info', 'acceptor')
|
||||||
|
|
||||||
|
|
||||||
class EventSlugField(serializers.SlugRelatedField):
|
class EventSlugField(serializers.SlugRelatedField):
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
|||||||
model = Voucher
|
model = Voucher
|
||||||
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
||||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||||
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat')
|
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
|
||||||
|
'all_bundles_included')
|
||||||
read_only_fields = ('id', 'redeemed')
|
read_only_fields = ('id', 'redeemed')
|
||||||
list_serializer_class = VoucherListSerializer
|
list_serializer_class = VoucherListSerializer
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class WaitingListSerializer(I18nAwareModelSerializer):
|
|||||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||||
full_data.update(data)
|
full_data.update(data)
|
||||||
|
|
||||||
WaitingListEntry.clean_duplicate(full_data.get('email'), full_data.get('item'), full_data.get('variation'),
|
WaitingListEntry.clean_duplicate(event, full_data.get('email'), full_data.get('item'), full_data.get('variation'),
|
||||||
full_data.get('subevent'), self.instance.pk if self.instance else None)
|
full_data.get('subevent'), self.instance.pk if self.instance else None)
|
||||||
WaitingListEntry.clean_itemvar(event, full_data.get('item'), full_data.get('variation'))
|
WaitingListEntry.clean_itemvar(event, full_data.get('item'), full_data.get('variation'))
|
||||||
WaitingListEntry.clean_subevent(event, full_data.get('subevent'))
|
WaitingListEntry.clean_subevent(event, full_data.get('subevent'))
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
|||||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||||
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
|
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
|
||||||
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
|
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
|
||||||
|
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
|
||||||
|
|
||||||
checkinlist_router = routers.DefaultRouter()
|
checkinlist_router = routers.DefaultRouter()
|
||||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
||||||
|
|||||||
@@ -562,7 +562,9 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
||||||
}, status=400)
|
}, status=400)
|
||||||
else:
|
else:
|
||||||
op_candidates = [media.linked_orderposition] + list(media.linked_orderposition.addons.all())
|
op_candidates = [media.linked_orderposition]
|
||||||
|
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
|
||||||
|
op_candidates += list(media.linked_orderposition.addons.all())
|
||||||
|
|
||||||
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
|
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
|
||||||
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
|
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ class InitializeView(APIView):
|
|||||||
if device.initialized:
|
if device.initialized:
|
||||||
raise ValidationError({'token': ['This initialization token has already been used.']})
|
raise ValidationError({'token': ['This initialization token has already been used.']})
|
||||||
|
|
||||||
|
if device.revoked:
|
||||||
|
raise ValidationError({'token': ['This initialization token has been revoked.']})
|
||||||
|
|
||||||
device.initialized = now()
|
device.initialized = now()
|
||||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
|
|||||||
@@ -47,11 +47,13 @@ from pretix.api.auth.permission import EventCRUDPermission
|
|||||||
from pretix.api.pagination import TotalOrderingFilter
|
from pretix.api.pagination import TotalOrderingFilter
|
||||||
from pretix.api.serializers.event import (
|
from pretix.api.serializers.event import (
|
||||||
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
||||||
EventSettingsSerializer, SubEventSerializer, TaxRuleSerializer,
|
EventSettingsSerializer, ItemMetaPropertiesSerializer, SubEventSerializer,
|
||||||
|
TaxRuleSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.views import ConditionalListView
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Device, Event, SeatCategoryMapping, TaxRule, TeamAPIToken,
|
CartPosition, Device, Event, ItemMetaProperty, SeatCategoryMapping,
|
||||||
|
TaxRule, TeamAPIToken,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
@@ -522,6 +524,54 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
|
class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = ItemMetaPropertiesSerializer
|
||||||
|
queryset = ItemMetaProperty.objects.none()
|
||||||
|
write_permission = 'can_change_event_settings'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = self.request.event.item_meta_properties.all()
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
ctx = super().get_serializer_context()
|
||||||
|
ctx['organizer'] = self.request.organizer
|
||||||
|
ctx['event'] = self.request.event
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
instance.log_action(
|
||||||
|
'pretix.event.item_meta_property.deleted',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data={'id': instance.pk}
|
||||||
|
)
|
||||||
|
instance.delete()
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
inst = serializer.save(event=self.request.event)
|
||||||
|
serializer.instance.log_action(
|
||||||
|
'pretix.event.item_meta_property.added',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data,
|
||||||
|
)
|
||||||
|
return inst
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
inst = serializer.save(event=self.request.event)
|
||||||
|
serializer.instance.log_action(
|
||||||
|
'pretix.event.item_meta_property.changed',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data=self.request.data,
|
||||||
|
)
|
||||||
|
return inst
|
||||||
|
|
||||||
|
|
||||||
class EventSettingsView(views.APIView):
|
class EventSettingsView(views.APIView):
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'can_change_event_settings'
|
write_permission = 'can_change_event_settings'
|
||||||
|
|||||||
@@ -155,7 +155,9 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
|||||||
qs = self.request.organizer.accepted_gift_cards
|
qs = self.request.organizer.accepted_gift_cards
|
||||||
else:
|
else:
|
||||||
qs = self.request.organizer.issued_gift_cards.all()
|
qs = self.request.organizer.issued_gift_cards.all()
|
||||||
return qs
|
return qs.prefetch_related(
|
||||||
|
'issuer'
|
||||||
|
)
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
@@ -166,7 +168,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
|||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
value = serializer.validated_data.pop('value')
|
value = serializer.validated_data.pop('value')
|
||||||
inst = serializer.save(issuer=self.request.organizer)
|
inst = serializer.save(issuer=self.request.organizer)
|
||||||
inst.transactions.create(value=value)
|
inst.transactions.create(value=value, acceptor=self.request.organizer)
|
||||||
inst.log_action(
|
inst.log_action(
|
||||||
'pretix.giftcards.transaction.manual',
|
'pretix.giftcards.transaction.manual',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
@@ -179,18 +181,32 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
|||||||
if 'include_accepted' in self.request.GET:
|
if 'include_accepted' in self.request.GET:
|
||||||
raise PermissionDenied("Accepted gift cards cannot be updated, use transact instead.")
|
raise PermissionDenied("Accepted gift cards cannot be updated, use transact instead.")
|
||||||
GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
|
GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
|
||||||
old_value = serializer.instance.value
|
|
||||||
value = serializer.validated_data.pop('value')
|
value = serializer.validated_data.pop('value', None)
|
||||||
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
|
|
||||||
testmode=serializer.instance.testmode)
|
if any(k != 'value' for k in self.request.data):
|
||||||
diff = value - old_value
|
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
|
||||||
inst.transactions.create(value=diff)
|
testmode=serializer.instance.testmode)
|
||||||
inst.log_action(
|
inst.log_action(
|
||||||
'pretix.giftcards.transaction.manual',
|
'pretix.giftcards.modified',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
data={'value': diff}
|
data=self.request.data,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
inst = serializer.instance
|
||||||
|
|
||||||
|
if 'value' in self.request.data and value is not None:
|
||||||
|
old_value = serializer.instance.value
|
||||||
|
diff = value - old_value
|
||||||
|
inst.transactions.create(value=diff, acceptor=self.request.organizer)
|
||||||
|
inst.log_action(
|
||||||
|
'pretix.giftcards.transaction.manual',
|
||||||
|
user=self.request.user,
|
||||||
|
auth=self.request.auth,
|
||||||
|
data={'value': diff}
|
||||||
|
)
|
||||||
|
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
@action(detail=True, methods=["POST"])
|
@action(detail=True, methods=["POST"])
|
||||||
@@ -203,18 +219,21 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
|||||||
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
|
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
|
||||||
request.data.get('text', '')
|
request.data.get('text', '')
|
||||||
)
|
)
|
||||||
|
info = serializers.JSONField(required=False, allow_null=True).to_internal_value(
|
||||||
|
request.data.get('info', {})
|
||||||
|
)
|
||||||
if gc.value + value < Decimal('0.00'):
|
if gc.value + value < Decimal('0.00'):
|
||||||
return Response({
|
return Response({
|
||||||
'value': ['The gift card does not have sufficient credit for this operation.']
|
'value': ['The gift card does not have sufficient credit for this operation.']
|
||||||
}, status=status.HTTP_409_CONFLICT)
|
}, status=status.HTTP_409_CONFLICT)
|
||||||
gc.transactions.create(value=value, text=text)
|
gc.transactions.create(value=value, text=text, info=info, acceptor=self.request.organizer)
|
||||||
gc.log_action(
|
gc.log_action(
|
||||||
'pretix.giftcards.transaction.manual',
|
'pretix.giftcards.transaction.manual',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
data={'value': value, 'text': text}
|
data={'value': value, 'text': text}
|
||||||
)
|
)
|
||||||
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
|
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
raise MethodNotAllowed("Gift cards cannot be deleted.")
|
raise MethodNotAllowed("Gift cards cannot be deleted.")
|
||||||
@@ -235,7 +254,7 @@ class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
return get_object_or_404(qs, pk=self.kwargs.get('giftcard'))
|
return get_object_or_404(qs, pk=self.kwargs.get('giftcard'))
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.giftcard.transactions.select_related('order', 'order__event')
|
return self.giftcard.transactions.select_related('order', 'order__event').prefetch_related('acceptor')
|
||||||
|
|
||||||
|
|
||||||
class TeamViewSet(viewsets.ModelViewSet):
|
class TeamViewSet(viewsets.ModelViewSet):
|
||||||
|
|||||||
@@ -117,13 +117,15 @@ def oidc_validate_and_complete_config(config):
|
|||||||
scopes=", ".join(provider_config.get("scopes_supported", []))
|
scopes=", ".join(provider_config.get("scopes_supported", []))
|
||||||
))
|
))
|
||||||
|
|
||||||
for k, v in config.items():
|
if "claims_supported" in provider_config:
|
||||||
if k.endswith('_field') and v:
|
claims_supported = provider_config.get("claims_supported", [])
|
||||||
if v not in provider_config.get("claims_supported", []): # https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
for k, v in config.items():
|
||||||
raise ValidationError(_('You are requesting field "{field}" but provider only supports these: {fields}.').format(
|
if k.endswith('_field') and v:
|
||||||
field=v,
|
if v not in claims_supported: # https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||||
fields=", ".join(provider_config.get("claims_supported", []))
|
raise ValidationError(_('You are requesting field "{field}" but provider only supports these: {fields}.').format(
|
||||||
))
|
field=v,
|
||||||
|
fields=", ".join(provider_config.get("claims_supported", []))
|
||||||
|
))
|
||||||
|
|
||||||
config['provider_config'] = provider_config
|
config['provider_config'] = provider_config
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'order': 'F8VVL',
|
'order': 'F8VVL',
|
||||||
'secret': '6zzjnumtsx136ddy',
|
'secret': '6zzjnumtsx136ddy',
|
||||||
'hash': '98kusd8ofsj8dnkd'
|
'hash': '98kusd8ofsj8dnkd'
|
||||||
},
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class EventDataExporter(ListExporter):
|
|||||||
_("Short form"),
|
_("Short form"),
|
||||||
_("Shop is live"),
|
_("Shop is live"),
|
||||||
_("Event currency"),
|
_("Event currency"),
|
||||||
|
_("Timezone"),
|
||||||
_("Event start time"),
|
_("Event start time"),
|
||||||
_("Event end time"),
|
_("Event end time"),
|
||||||
_("Admission time"),
|
_("Admission time"),
|
||||||
@@ -75,16 +76,18 @@ class EventDataExporter(ListExporter):
|
|||||||
|
|
||||||
for e in self.events.all():
|
for e in self.events.all():
|
||||||
m = e.meta_data
|
m = e.meta_data
|
||||||
|
tz = e.timezone
|
||||||
yield [
|
yield [
|
||||||
str(e.name),
|
str(e.name),
|
||||||
e.slug,
|
e.slug,
|
||||||
_('Yes') if e.live else _('No'),
|
_('Yes') if e.live else _('No'),
|
||||||
e.currency,
|
e.currency,
|
||||||
date_format(e.date_from, 'SHORT_DATETIME_FORMAT'),
|
str(e.timezone),
|
||||||
date_format(e.date_to, 'SHORT_DATETIME_FORMAT') if e.date_to else '',
|
date_format(e.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
|
||||||
date_format(e.date_admission, 'SHORT_DATETIME_FORMAT') if e.date_admission else '',
|
date_format(e.date_to.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.date_to else '',
|
||||||
date_format(e.presale_start, 'SHORT_DATETIME_FORMAT') if e.presale_start else '',
|
date_format(e.date_admission.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.date_admission else '',
|
||||||
date_format(e.presale_end, 'SHORT_DATETIME_FORMAT') if e.presale_end else '',
|
date_format(e.presale_start.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.presale_start else '',
|
||||||
|
date_format(e.presale_end.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.presale_end else '',
|
||||||
str(e.location),
|
str(e.location),
|
||||||
e.geo_lat or '',
|
e.geo_lat or '',
|
||||||
e.geo_lon or '',
|
e.geo_lon or '',
|
||||||
|
|||||||
@@ -103,7 +103,9 @@ class InvoiceExporterMixin:
|
|||||||
qs = qs.annotate(
|
qs = qs.annotate(
|
||||||
has_payment_with_provider=Exists(
|
has_payment_with_provider=Exists(
|
||||||
OrderPayment.objects.filter(
|
OrderPayment.objects.filter(
|
||||||
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider'))
|
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider')),
|
||||||
|
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
|
||||||
|
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,18 +49,24 @@ from django.utils.timezone import get_current_timezone, now
|
|||||||
from django.utils.translation import (
|
from django.utils.translation import (
|
||||||
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
|
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
|
||||||
)
|
)
|
||||||
|
from openpyxl.cell import WriteOnlyCell
|
||||||
|
from openpyxl.comments import Comment
|
||||||
|
from openpyxl.styles import Font, PatternFill
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
||||||
OrderPosition, Question,
|
OrderPosition, Question,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
from pretix.base.models.orders import (
|
||||||
|
OrderFee, OrderPayment, OrderRefund, Transaction,
|
||||||
|
)
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
|
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
|
||||||
|
|
||||||
from ...control.forms.filter import get_all_payment_providers
|
from ...control.forms.filter import get_all_payment_providers
|
||||||
from ...helpers import GroupConcat
|
from ...helpers import GroupConcat
|
||||||
from ...helpers.iter import chunked_iterable
|
from ...helpers.iter import chunked_iterable
|
||||||
|
from ...helpers.safe_openpyxl import remove_invalid_excel_chars
|
||||||
from ..exporter import (
|
from ..exporter import (
|
||||||
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
|
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
|
||||||
)
|
)
|
||||||
@@ -759,6 +765,181 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
return '{}_orders'.format(self.event.slug)
|
return '{}_orders'.format(self.event.slug)
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionListExporter(ListExporter):
|
||||||
|
identifier = 'transactions'
|
||||||
|
verbose_name = gettext_lazy('Order transaction data')
|
||||||
|
category = pgettext_lazy('export_category', 'Order data')
|
||||||
|
description = gettext_lazy('Download a spreadsheet of all substantial changes to orders, i.e. all changes to '
|
||||||
|
'products, prices or tax rates. The information is only accurate for changes made with '
|
||||||
|
'pretix versions released after October 2021.')
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def providers(self):
|
||||||
|
return dict(get_all_payment_providers())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def additional_form_fields(self):
|
||||||
|
d = [
|
||||||
|
('date_range',
|
||||||
|
DateFrameField(
|
||||||
|
label=_('Date range'),
|
||||||
|
include_future_frames=False,
|
||||||
|
required=False,
|
||||||
|
help_text=_('Only include transactions created within this date range.')
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
d = OrderedDict(d)
|
||||||
|
return d
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def event_object_cache(self):
|
||||||
|
return {e.pk: e for e in self.events}
|
||||||
|
|
||||||
|
def get_filename(self):
|
||||||
|
if self.is_multievent:
|
||||||
|
return '{}_transactions'.format(self.organizer.slug)
|
||||||
|
else:
|
||||||
|
return '{}_transactions'.format(self.event.slug)
|
||||||
|
|
||||||
|
def iterate_list(self, form_data):
|
||||||
|
qs = Transaction.objects.filter(
|
||||||
|
order__event__in=self.events,
|
||||||
|
)
|
||||||
|
|
||||||
|
if form_data.get('date_range'):
|
||||||
|
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
|
||||||
|
if dt_start:
|
||||||
|
qs = qs.filter(datetime__gte=dt_start)
|
||||||
|
if dt_end:
|
||||||
|
qs = qs.filter(datetime__lt=dt_end)
|
||||||
|
|
||||||
|
qs = qs.select_related(
|
||||||
|
'order', 'order__event', 'item', 'variation', 'subevent',
|
||||||
|
).order_by(
|
||||||
|
'datetime', 'id',
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
_('Event'),
|
||||||
|
_('Event slug'),
|
||||||
|
_('Currency'),
|
||||||
|
|
||||||
|
_('Order code'),
|
||||||
|
_('Order date'),
|
||||||
|
_('Order time'),
|
||||||
|
|
||||||
|
_('Transaction date'),
|
||||||
|
_('Transaction time'),
|
||||||
|
_('Old data'),
|
||||||
|
|
||||||
|
_('Position ID'),
|
||||||
|
_('Quantity'),
|
||||||
|
|
||||||
|
_('Product ID'),
|
||||||
|
_('Product'),
|
||||||
|
_('Variation ID'),
|
||||||
|
_('Variation'),
|
||||||
|
_('Fee type'),
|
||||||
|
_('Internal fee type'),
|
||||||
|
|
||||||
|
pgettext('subevent', 'Date ID'),
|
||||||
|
pgettext('subevent', 'Date'),
|
||||||
|
|
||||||
|
_('Price'),
|
||||||
|
_('Tax rate'),
|
||||||
|
_('Tax rule ID'),
|
||||||
|
_('Tax rule'),
|
||||||
|
_('Tax value'),
|
||||||
|
]
|
||||||
|
|
||||||
|
if form_data.get('_format') == 'xlsx':
|
||||||
|
for i in range(len(headers)):
|
||||||
|
headers[i] = WriteOnlyCell(self.__ws, value=headers[i])
|
||||||
|
if i in (0, 12, 14, 18, 22):
|
||||||
|
headers[i].fill = PatternFill(start_color="FFB419", end_color="FFB419", fill_type="solid")
|
||||||
|
headers[i].comment = Comment(
|
||||||
|
text=_(
|
||||||
|
"This value is supplied for informational purposes, it is not part of the original transaction "
|
||||||
|
"data and might have changed since the transaction."
|
||||||
|
),
|
||||||
|
author='system'
|
||||||
|
)
|
||||||
|
headers[i].font = Font(bold=True)
|
||||||
|
|
||||||
|
yield headers
|
||||||
|
|
||||||
|
yield self.ProgressSetTotal(total=qs.count())
|
||||||
|
|
||||||
|
for t in qs.iterator():
|
||||||
|
row = [
|
||||||
|
str(t.order.event.name),
|
||||||
|
t.order.event.slug,
|
||||||
|
t.order.event.currency,
|
||||||
|
|
||||||
|
t.order.code,
|
||||||
|
t.order.datetime.astimezone(self.timezone).strftime('%Y-%m-%d'),
|
||||||
|
t.order.datetime.astimezone(self.timezone).strftime('%H:%M:%S'),
|
||||||
|
|
||||||
|
t.datetime.astimezone(self.timezone).strftime('%Y-%m-%d'),
|
||||||
|
t.datetime.astimezone(self.timezone).strftime('%H:%M:%S'),
|
||||||
|
_('Converted from legacy version') if t.migrated else '',
|
||||||
|
|
||||||
|
t.positionid,
|
||||||
|
t.count,
|
||||||
|
|
||||||
|
t.item_id,
|
||||||
|
str(t.item),
|
||||||
|
t.variation_id or '',
|
||||||
|
str(t.variation) if t.variation_id else '',
|
||||||
|
t.fee_type,
|
||||||
|
t.internal_type,
|
||||||
|
t.subevent_id or '',
|
||||||
|
str(t.subevent) if t.subevent else '',
|
||||||
|
|
||||||
|
t.price,
|
||||||
|
t.tax_rate,
|
||||||
|
t.tax_rule_id or '',
|
||||||
|
str(t.tax_rule.internal_name or t.tax_rule.name) if t.tax_rule_id else '',
|
||||||
|
t.tax_value,
|
||||||
|
]
|
||||||
|
|
||||||
|
if form_data.get('_format') == 'xlsx':
|
||||||
|
for i in range(len(row)):
|
||||||
|
if t.order.testmode:
|
||||||
|
row[i] = WriteOnlyCell(self.__ws, value=remove_invalid_excel_chars(row[i]))
|
||||||
|
row[i].fill = PatternFill(start_color="FFB419", end_color="FFB419", fill_type="solid")
|
||||||
|
|
||||||
|
yield row
|
||||||
|
|
||||||
|
def prepare_xlsx_sheet(self, ws):
|
||||||
|
self.__ws = ws
|
||||||
|
ws.freeze_panes = 'A2'
|
||||||
|
ws.column_dimensions['A'].width = 25
|
||||||
|
ws.column_dimensions['B'].width = 10
|
||||||
|
ws.column_dimensions['C'].width = 10
|
||||||
|
ws.column_dimensions['D'].width = 10
|
||||||
|
ws.column_dimensions['E'].width = 15
|
||||||
|
ws.column_dimensions['F'].width = 15
|
||||||
|
ws.column_dimensions['G'].width = 15
|
||||||
|
ws.column_dimensions['H'].width = 15
|
||||||
|
ws.column_dimensions['I'].width = 15
|
||||||
|
ws.column_dimensions['J'].width = 10
|
||||||
|
ws.column_dimensions['K'].width = 10
|
||||||
|
ws.column_dimensions['L'].width = 10
|
||||||
|
ws.column_dimensions['M'].width = 25
|
||||||
|
ws.column_dimensions['N'].width = 10
|
||||||
|
ws.column_dimensions['O'].width = 25
|
||||||
|
ws.column_dimensions['P'].width = 20
|
||||||
|
ws.column_dimensions['Q'].width = 20
|
||||||
|
ws.column_dimensions['R'].width = 10
|
||||||
|
ws.column_dimensions['S'].width = 25
|
||||||
|
ws.column_dimensions['T'].width = 15
|
||||||
|
ws.column_dimensions['U'].width = 10
|
||||||
|
ws.column_dimensions['V'].width = 10
|
||||||
|
ws.column_dimensions['W'].width = 20
|
||||||
|
ws.column_dimensions['X'].width = 15
|
||||||
|
|
||||||
|
|
||||||
class PaymentListExporter(ListExporter):
|
class PaymentListExporter(ListExporter):
|
||||||
identifier = 'paymentlist'
|
identifier = 'paymentlist'
|
||||||
verbose_name = gettext_lazy('Payments and refunds')
|
verbose_name = gettext_lazy('Payments and refunds')
|
||||||
@@ -1165,6 +1346,16 @@ def register_multievent_orderlist_exporter(sender, **kwargs):
|
|||||||
return OrderListExporter
|
return OrderListExporter
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_data_exporters, dispatch_uid="exporter_ordertransactionlist")
|
||||||
|
def register_ordertransactionlist_exporter(sender, **kwargs):
|
||||||
|
return TransactionListExporter
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_ordertransactionlist")
|
||||||
|
def register_multievent_ordertransactionlist_exporter(sender, **kwargs):
|
||||||
|
return TransactionListExporter
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_data_exporters, dispatch_uid="exporter_paymentlist")
|
@receiver(register_data_exporters, dispatch_uid="exporter_paymentlist")
|
||||||
def register_paymentlist_exporter(sender, **kwargs):
|
def register_paymentlist_exporter(sender, **kwargs):
|
||||||
return PaymentListExporter
|
return PaymentListExporter
|
||||||
|
|||||||
@@ -573,6 +573,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
the attendee name for admission tickets, if the corresponding setting is enabled,
|
the attendee name for admission tickets, if the corresponding setting is enabled,
|
||||||
as well as additional questions defined by the organizer.
|
as well as additional questions defined by the organizer.
|
||||||
"""
|
"""
|
||||||
|
address_validation = False
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -920,8 +921,14 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '')
|
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '')
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
from pretix.base.addressvalidation import \
|
||||||
|
validate_address # local import to prevent impact on startup time
|
||||||
|
|
||||||
d = super().clean()
|
d = super().clean()
|
||||||
|
|
||||||
|
if self.address_validation:
|
||||||
|
self.cleaned_data = d = validate_address(d, True)
|
||||||
|
|
||||||
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
if not d.get('state'):
|
if not d.get('state'):
|
||||||
self.add_error('state', _('This field is required.'))
|
self.add_error('state', _('This field is required.'))
|
||||||
|
|||||||
+143
-80
@@ -20,6 +20,8 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@@ -28,6 +30,7 @@ from typing import Tuple
|
|||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
import vat_moss.exchange_rates
|
import vat_moss.exchange_rates
|
||||||
|
from bidi.algorithm import get_display
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
@@ -53,7 +56,8 @@ from pretix.base.models import Event, Invoice, Order, OrderPayment
|
|||||||
from pretix.base.services.currencies import SOURCE_NAMES
|
from pretix.base.services.currencies import SOURCE_NAMES
|
||||||
from pretix.base.signals import register_invoice_renderers
|
from pretix.base.signals import register_invoice_renderers
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.helpers.reportlab import ThumbnailingImageReader
|
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
|
||||||
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -79,7 +83,12 @@ class NumberedCanvas(Canvas):
|
|||||||
def draw_page_number(self, page_count):
|
def draw_page_number(self, page_count):
|
||||||
self.saveState()
|
self.saveState()
|
||||||
self.setFont(self.font_regular, 8)
|
self.setFont(self.font_regular, 8)
|
||||||
self.drawRightString(self._pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d of %d") % (self._pageNumber, page_count,))
|
text = pgettext("invoice", "Page %d of %d") % (self._pageNumber, page_count,)
|
||||||
|
try:
|
||||||
|
text = get_display(reshaper.reshape(text))
|
||||||
|
except:
|
||||||
|
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||||
|
self.drawRightString(self._pagesize[0] - 20 * mm, 10 * mm, text)
|
||||||
self.restoreState()
|
self.restoreState()
|
||||||
|
|
||||||
|
|
||||||
@@ -139,8 +148,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
"""
|
"""
|
||||||
Initialize the renderer. By default, this registers fonts and sets ``self.stylesheet``.
|
Initialize the renderer. By default, this registers fonts and sets ``self.stylesheet``.
|
||||||
"""
|
"""
|
||||||
self.stylesheet = self._get_stylesheet()
|
|
||||||
self._register_fonts()
|
self._register_fonts()
|
||||||
|
self.stylesheet = self._get_stylesheet()
|
||||||
|
|
||||||
def _get_stylesheet(self):
|
def _get_stylesheet(self):
|
||||||
"""
|
"""
|
||||||
@@ -148,6 +157,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
"""
|
"""
|
||||||
stylesheet = StyleSheet1()
|
stylesheet = StyleSheet1()
|
||||||
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12))
|
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12))
|
||||||
|
stylesheet.add(ParagraphStyle(name='Bold', fontName=self.font_bold, fontSize=10, leading=12))
|
||||||
|
stylesheet.add(ParagraphStyle(name='BoldRight', fontName=self.font_bold, fontSize=10, leading=12, alignment=TA_RIGHT))
|
||||||
|
stylesheet.add(ParagraphStyle(name='BoldRightNoSplit', fontName=self.font_bold, fontSize=10, leading=12, alignment=TA_RIGHT,
|
||||||
|
splitLongWords=False))
|
||||||
stylesheet.add(ParagraphStyle(name='NormalRight', fontName=self.font_regular, fontSize=10, leading=12, alignment=TA_RIGHT))
|
stylesheet.add(ParagraphStyle(name='NormalRight', fontName=self.font_regular, fontSize=10, leading=12, alignment=TA_RIGHT))
|
||||||
stylesheet.add(ParagraphStyle(name='BoldInverseCenter', fontName=self.font_bold, fontSize=10, leading=12,
|
stylesheet.add(ParagraphStyle(name='BoldInverseCenter', fontName=self.font_bold, fontSize=10, leading=12,
|
||||||
textColor=colors.white, alignment=TA_CENTER))
|
textColor=colors.white, alignment=TA_CENTER))
|
||||||
@@ -155,6 +168,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
stylesheet.add(ParagraphStyle(name='Heading1', fontName=self.font_bold, fontSize=15, leading=15 * 1.2))
|
stylesheet.add(ParagraphStyle(name='Heading1', fontName=self.font_bold, fontSize=15, leading=15 * 1.2))
|
||||||
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName=self.font_bold, fontSize=8, leading=12))
|
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName=self.font_bold, fontSize=8, leading=12))
|
||||||
stylesheet.add(ParagraphStyle(name='Fineprint', fontName=self.font_regular, fontSize=8, leading=10))
|
stylesheet.add(ParagraphStyle(name='Fineprint', fontName=self.font_regular, fontSize=8, leading=10))
|
||||||
|
stylesheet.add(ParagraphStyle(name='FineprintRight', fontName=self.font_regular, fontSize=8, leading=10, alignment=TA_RIGHT))
|
||||||
return stylesheet
|
return stylesheet
|
||||||
|
|
||||||
def _register_fonts(self):
|
def _register_fonts(self):
|
||||||
@@ -168,6 +182,32 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
|
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
|
||||||
italic='OpenSansIt', boldItalic='OpenSansBI')
|
italic='OpenSansIt', boldItalic='OpenSansBI')
|
||||||
|
|
||||||
|
for family, styles in get_fonts().items():
|
||||||
|
if family == self.event.settings.invoice_renderer_font:
|
||||||
|
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
||||||
|
self.font_regular = family
|
||||||
|
if 'italic' in styles:
|
||||||
|
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
|
||||||
|
if 'bold' in styles:
|
||||||
|
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
|
||||||
|
self.font_bold = family + ' B'
|
||||||
|
if 'bolditalic' in styles:
|
||||||
|
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
||||||
|
|
||||||
|
def _normalize(self, text):
|
||||||
|
# reportlab does not support unicode combination characters
|
||||||
|
# It's important we do this before we use ArabicReshaper
|
||||||
|
text = unicodedata.normalize("NFKC", text)
|
||||||
|
|
||||||
|
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
||||||
|
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||||
|
try:
|
||||||
|
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
|
||||||
|
except:
|
||||||
|
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
def _upper(self, val):
|
def _upper(self, val):
|
||||||
# We uppercase labels, but not in every language
|
# We uppercase labels, but not in every language
|
||||||
if get_language().startswith('el'):
|
if get_language().startswith('el'):
|
||||||
@@ -247,10 +287,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
return 'invoice.pdf', 'application/pdf', buffer.read()
|
return 'invoice.pdf', 'application/pdf', buffer.read()
|
||||||
|
|
||||||
def _clean_text(self, text, tags=None):
|
def _clean_text(self, text, tags=None):
|
||||||
return bleach.clean(
|
return self._normalize(bleach.clean(
|
||||||
text,
|
text,
|
||||||
tags=tags or []
|
tags=tags or []
|
||||||
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
|
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
||||||
|
|
||||||
|
|
||||||
class PaidMarker(Flowable):
|
class PaidMarker(Flowable):
|
||||||
@@ -291,7 +331,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
canvas.setFont(self.font_regular, 8)
|
canvas.setFont(self.font_regular, 8)
|
||||||
|
|
||||||
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
|
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
|
||||||
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
|
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, self._normalize(line.strip()))
|
||||||
|
|
||||||
canvas.restoreState()
|
canvas.restoreState()
|
||||||
|
|
||||||
@@ -324,13 +364,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
def _draw_invoice_from_label(self, canvas):
|
def _draw_invoice_from_label(self, canvas):
|
||||||
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
|
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
|
||||||
textobject.setFont(self.font_bold, 8)
|
textobject.setFont(self.font_bold, 8)
|
||||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice from')))
|
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice from'))))
|
||||||
canvas.drawText(textobject)
|
canvas.drawText(textobject)
|
||||||
|
|
||||||
def _draw_invoice_to_label(self, canvas):
|
def _draw_invoice_to_label(self, canvas):
|
||||||
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
|
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
|
||||||
textobject.setFont(self.font_bold, 8)
|
textobject.setFont(self.font_bold, 8)
|
||||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice to')))
|
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice to'))))
|
||||||
canvas.drawText(textobject)
|
canvas.drawText(textobject)
|
||||||
|
|
||||||
logo_width = 25 * mm
|
logo_width = 25 * mm
|
||||||
@@ -358,51 +398,51 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
def _draw_metadata(self, canvas):
|
def _draw_metadata(self, canvas):
|
||||||
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
|
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
|
||||||
textobject.setFont(self.font_bold, 8)
|
textobject.setFont(self.font_bold, 8)
|
||||||
textobject.textLine(self._upper(pgettext('invoice', 'Order code')))
|
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Order code'))))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
textobject.setFont(self.font_regular, 10)
|
textobject.setFont(self.font_regular, 10)
|
||||||
textobject.textLine(self.invoice.order.full_code)
|
textobject.textLine(self._normalize(self.invoice.order.full_code))
|
||||||
canvas.drawText(textobject)
|
canvas.drawText(textobject)
|
||||||
|
|
||||||
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
|
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
|
||||||
textobject.setFont(self.font_bold, 8)
|
textobject.setFont(self.font_bold, 8)
|
||||||
if self.invoice.is_cancellation:
|
if self.invoice.is_cancellation:
|
||||||
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation number')))
|
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Cancellation number'))))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
textobject.setFont(self.font_regular, 10)
|
textobject.setFont(self.font_regular, 10)
|
||||||
textobject.textLine(self.invoice.number)
|
textobject.textLine(self._normalize(self.invoice.number))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
textobject.setFont(self.font_bold, 8)
|
textobject.setFont(self.font_bold, 8)
|
||||||
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice')))
|
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Original invoice'))))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
textobject.setFont(self.font_regular, 10)
|
textobject.setFont(self.font_regular, 10)
|
||||||
textobject.textLine(self.invoice.refers.number)
|
textobject.textLine(self._normalize(self.invoice.refers.number))
|
||||||
else:
|
else:
|
||||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice number')))
|
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice number'))))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
textobject.setFont(self.font_regular, 10)
|
textobject.setFont(self.font_regular, 10)
|
||||||
textobject.textLine(self.invoice.number)
|
textobject.textLine(self._normalize(self.invoice.number))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
|
|
||||||
if self.invoice.is_cancellation:
|
if self.invoice.is_cancellation:
|
||||||
textobject.setFont(self.font_bold, 8)
|
textobject.setFont(self.font_bold, 8)
|
||||||
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation date')))
|
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Cancellation date'))))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
textobject.setFont(self.font_regular, 10)
|
textobject.setFont(self.font_regular, 10)
|
||||||
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
|
textobject.textLine(self._normalize(date_format(self.invoice.date, "DATE_FORMAT")))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
textobject.setFont(self.font_bold, 8)
|
textobject.setFont(self.font_bold, 8)
|
||||||
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice date')))
|
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Original invoice date'))))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
textobject.setFont(self.font_regular, 10)
|
textobject.setFont(self.font_regular, 10)
|
||||||
textobject.textLine(date_format(self.invoice.refers.date, "DATE_FORMAT"))
|
textobject.textLine(self._normalize(date_format(self.invoice.refers.date, "DATE_FORMAT")))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
else:
|
else:
|
||||||
textobject.setFont(self.font_bold, 8)
|
textobject.setFont(self.font_bold, 8)
|
||||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice date')))
|
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice date'))))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
textobject.setFont(self.font_regular, 10)
|
textobject.setFont(self.font_regular, 10)
|
||||||
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
|
textobject.textLine(self._normalize(date_format(self.invoice.date, "DATE_FORMAT")))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
|
|
||||||
canvas.drawText(textobject)
|
canvas.drawText(textobject)
|
||||||
@@ -415,19 +455,19 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
def _draw_event_label(self, canvas):
|
def _draw_event_label(self, canvas):
|
||||||
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
|
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
|
||||||
textobject.setFont(self.font_bold, 8)
|
textobject.setFont(self.font_bold, 8)
|
||||||
textobject.textLine(self._upper(pgettext('invoice', 'Event')))
|
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Event'))))
|
||||||
canvas.drawText(textobject)
|
canvas.drawText(textobject)
|
||||||
|
|
||||||
def _draw_event(self, canvas):
|
def _draw_event(self, canvas):
|
||||||
def shorten(txt):
|
def shorten(txt):
|
||||||
txt = str(txt)
|
txt = str(txt)
|
||||||
txt = bleach.clean(txt, tags=[]).strip()
|
txt = bleach.clean(txt, tags=[]).strip()
|
||||||
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
|
|
||||||
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
||||||
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
||||||
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
@@ -453,7 +493,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
else:
|
else:
|
||||||
p_str = shorten(self.invoice.event.name)
|
p_str = shorten(self.invoice.event.name)
|
||||||
|
|
||||||
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
p = Paragraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||||
p.wrapOn(canvas, self.event_width, self.event_height)
|
p.wrapOn(canvas, self.event_width, self.event_height)
|
||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
|
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
|
||||||
@@ -462,14 +502,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
def _draw_footer(self, canvas):
|
def _draw_footer(self, canvas):
|
||||||
canvas.setFont(self.font_regular, 8)
|
canvas.setFont(self.font_regular, 8)
|
||||||
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
|
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
|
||||||
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
|
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, self._normalize(line.strip()))
|
||||||
|
|
||||||
def _draw_testmode(self, canvas):
|
def _draw_testmode(self, canvas):
|
||||||
if self.invoice.order.testmode:
|
if self.invoice.order.testmode:
|
||||||
canvas.saveState()
|
canvas.saveState()
|
||||||
canvas.setFont('OpenSansBd', 30)
|
canvas.setFont(self.font_bold, 30)
|
||||||
canvas.setFillColorRGB(32, 0, 0)
|
canvas.setFillColorRGB(32, 0, 0)
|
||||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE'))
|
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, self._normalize(gettext('TEST MODE')))
|
||||||
canvas.restoreState()
|
canvas.restoreState()
|
||||||
|
|
||||||
def _on_first_page(self, canvas: Canvas, doc):
|
def _on_first_page(self, canvas: Canvas, doc):
|
||||||
@@ -517,22 +557,22 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
if self.invoice.internal_reference:
|
if self.invoice.internal_reference:
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
pgettext('invoice', 'Customer reference: {reference}').format(
|
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
|
||||||
reference=self._clean_text(self.invoice.internal_reference),
|
reference=self._clean_text(self.invoice.internal_reference),
|
||||||
),
|
)),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.invoice_to_vat_id:
|
if self.invoice.invoice_to_vat_id:
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
pgettext('invoice', 'Customer VAT ID') + ': ' +
|
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
|
||||||
self._clean_text(self.invoice.invoice_to_vat_id),
|
self._clean_text(self.invoice.invoice_to_vat_id),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.invoice_to_beneficiary:
|
if self.invoice.invoice_to_beneficiary:
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
pgettext('invoice', 'Beneficiary') + ':<br />' +
|
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
|
||||||
self._clean_text(self.invoice.invoice_to_beneficiary),
|
self._clean_text(self.invoice.invoice_to_beneficiary),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
@@ -552,10 +592,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
story = [
|
story = [
|
||||||
NextPageTemplate('FirstPage'),
|
NextPageTemplate('FirstPage'),
|
||||||
Paragraph(
|
Paragraph(
|
||||||
(
|
self._normalize(
|
||||||
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
||||||
else pgettext('invoice', 'Invoice')
|
else pgettext('invoice', 'Invoice')
|
||||||
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
|
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')),
|
||||||
self.stylesheet['Heading1']
|
self.stylesheet['Heading1']
|
||||||
),
|
),
|
||||||
Spacer(1, 5 * mm),
|
Spacer(1, 5 * mm),
|
||||||
@@ -577,17 +617,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
]
|
]
|
||||||
if has_taxes:
|
if has_taxes:
|
||||||
tdata = [(
|
tdata = [(
|
||||||
pgettext('invoice', 'Description'),
|
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||||
pgettext('invoice', 'Qty'),
|
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||||
pgettext('invoice', 'Tax rate'),
|
Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
|
||||||
pgettext('invoice', 'Net'),
|
Paragraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
|
||||||
pgettext('invoice', 'Gross'),
|
Paragraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
|
||||||
)]
|
)]
|
||||||
else:
|
else:
|
||||||
tdata = [(
|
tdata = [(
|
||||||
pgettext('invoice', 'Description'),
|
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['BoldRight']),
|
||||||
pgettext('invoice', 'Qty'),
|
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||||
pgettext('invoice', 'Amount'),
|
Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
|
||||||
)]
|
)]
|
||||||
|
|
||||||
def _group_key(line):
|
def _group_key(line):
|
||||||
@@ -634,13 +674,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
if has_taxes:
|
if has_taxes:
|
||||||
tdata.append([
|
tdata.append([
|
||||||
pgettext('invoice', 'Invoice total'), '', '', '',
|
Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
|
||||||
money_filter(total, self.invoice.event.currency)
|
money_filter(total, self.invoice.event.currency)
|
||||||
])
|
])
|
||||||
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
|
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
|
||||||
else:
|
else:
|
||||||
tdata.append([
|
tdata.append([
|
||||||
pgettext('invoice', 'Invoice total'), '',
|
Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
|
||||||
money_filter(total, self.invoice.event.currency)
|
money_filter(total, self.invoice.event.currency)
|
||||||
])
|
])
|
||||||
colwidths = [a * doc.width for a in (.65, .20, .15)]
|
colwidths = [a * doc.width for a in (.65, .20, .15)]
|
||||||
@@ -649,12 +689,16 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING:
|
if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING:
|
||||||
pending_sum = self.invoice.order.pending_sum
|
pending_sum = self.invoice.order.pending_sum
|
||||||
if pending_sum != total:
|
if pending_sum != total:
|
||||||
tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [
|
tdata.append(
|
||||||
money_filter(pending_sum - total, self.invoice.event.currency)
|
[Paragraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
|
||||||
])
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
tdata.append([pgettext('invoice', 'Outstanding payments')] + (['', '', ''] if has_taxes else ['']) + [
|
[money_filter(pending_sum - total, self.invoice.event.currency)]
|
||||||
money_filter(pending_sum, self.invoice.event.currency)
|
)
|
||||||
])
|
tdata.append(
|
||||||
|
[Paragraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
|
||||||
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
|
[money_filter(pending_sum, self.invoice.event.currency)]
|
||||||
|
)
|
||||||
tstyledata += [
|
tstyledata += [
|
||||||
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
|
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
|
||||||
]
|
]
|
||||||
@@ -667,19 +711,24 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
).aggregate(
|
).aggregate(
|
||||||
s=Sum('amount')
|
s=Sum('amount')
|
||||||
)['s'] or Decimal('0.00')
|
)['s'] or Decimal('0.00')
|
||||||
tdata.append([pgettext('invoice', 'Paid by gift card')] + (['', '', ''] if has_taxes else ['']) + [
|
tdata.append(
|
||||||
money_filter(giftcard_sum, self.invoice.event.currency)
|
[Paragraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
|
||||||
])
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
tdata.append([pgettext('invoice', 'Remaining amount')] + (['', '', ''] if has_taxes else ['']) + [
|
[money_filter(giftcard_sum, self.invoice.event.currency)]
|
||||||
money_filter(total - giftcard_sum, self.invoice.event.currency)
|
)
|
||||||
])
|
tdata.append(
|
||||||
|
[Paragraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
|
||||||
|
(['', '', ''] if has_taxes else ['']) +
|
||||||
|
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
|
||||||
|
)
|
||||||
tstyledata += [
|
tstyledata += [
|
||||||
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
|
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
|
||||||
]
|
]
|
||||||
elif self.invoice.payment_provider_stamp:
|
elif self.invoice.payment_provider_stamp:
|
||||||
pm = PaidMarker(
|
pm = PaidMarker(
|
||||||
text=self.invoice.payment_provider_stamp,
|
text=self._normalize(self.invoice.payment_provider_stamp),
|
||||||
color=colors.HexColor(self.event.settings.theme_color_success),
|
color=colors.HexColor(self.event.settings.theme_color_success),
|
||||||
|
font=self.font_bold,
|
||||||
size=16
|
size=16
|
||||||
)
|
)
|
||||||
tdata[-1][-2] = pm
|
tdata[-1][-2] = pm
|
||||||
@@ -692,7 +741,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
if self.invoice.payment_provider_text:
|
if self.invoice.payment_provider_text:
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
self.invoice.payment_provider_text,
|
self._normalize(self.invoice.payment_provider_text),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -716,10 +765,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
||||||
]
|
]
|
||||||
thead = [
|
thead = [
|
||||||
pgettext('invoice', 'Tax rate'),
|
Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
|
||||||
pgettext('invoice', 'Net value'),
|
Paragraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
|
||||||
pgettext('invoice', 'Gross value'),
|
Paragraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
|
||||||
pgettext('invoice', 'Tax'),
|
Paragraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
|
||||||
''
|
''
|
||||||
]
|
]
|
||||||
tdata = [thead]
|
tdata = [thead]
|
||||||
@@ -730,7 +779,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
continue
|
continue
|
||||||
tax = taxvalue_map[idx]
|
tax = taxvalue_map[idx]
|
||||||
tdata.append([
|
tdata.append([
|
||||||
localize(rate) + " % " + name,
|
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||||
money_filter(gross - tax, self.invoice.event.currency),
|
money_filter(gross - tax, self.invoice.event.currency),
|
||||||
money_filter(gross, self.invoice.event.currency),
|
money_filter(gross, self.invoice.event.currency),
|
||||||
money_filter(tax, self.invoice.event.currency),
|
money_filter(tax, self.invoice.event.currency),
|
||||||
@@ -749,7 +798,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
table.setStyle(TableStyle(tstyledata))
|
table.setStyle(TableStyle(tstyledata))
|
||||||
story.append(Spacer(5 * mm, 5 * mm))
|
story.append(Spacer(5 * mm, 5 * mm))
|
||||||
story.append(KeepTogether([
|
story.append(KeepTogether([
|
||||||
Paragraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
|
Paragraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
|
||||||
table
|
table
|
||||||
]))
|
]))
|
||||||
|
|
||||||
@@ -766,7 +815,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
net = gross - tax
|
net = gross - tax
|
||||||
|
|
||||||
tdata.append([
|
tdata.append([
|
||||||
localize(rate) + " % " + name,
|
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||||
fmt(net), fmt(gross), fmt(tax), ''
|
fmt(net), fmt(gross), fmt(tax), ''
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -776,12 +825,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
story.append(KeepTogether([
|
story.append(KeepTogether([
|
||||||
Spacer(1, height=2 * mm),
|
Spacer(1, height=2 * mm),
|
||||||
Paragraph(
|
Paragraph(
|
||||||
pgettext(
|
self._normalize(pgettext(
|
||||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||||
'{date}, this corresponds to:'
|
'{date}, this corresponds to:'
|
||||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||||
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
||||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
|
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))),
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
),
|
),
|
||||||
Spacer(1, height=3 * mm),
|
Spacer(1, height=3 * mm),
|
||||||
@@ -790,14 +839,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
||||||
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
|
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
|
||||||
story.append(Spacer(1, 5 * mm))
|
story.append(Spacer(1, 5 * mm))
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(self._normalize(
|
||||||
pgettext(
|
pgettext(
|
||||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||||
'{date}, the invoice total corresponds to {total}.'
|
'{date}, the invoice total corresponds to {total}.'
|
||||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
|
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
|
||||||
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
||||||
total=fmt(foreign_total)),
|
total=fmt(foreign_total))),
|
||||||
self.stylesheet['Fineprint']
|
self.stylesheet['Fineprint']
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -843,7 +892,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
|||||||
self._clean_text(l)
|
self._clean_text(l)
|
||||||
for l in self.invoice.address_invoice_from.strip().split('\n')
|
for l in self.invoice.address_invoice_from.strip().split('\n')
|
||||||
]
|
]
|
||||||
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
|
p = Paragraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
|
||||||
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
|
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
|
||||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
|
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
|
||||||
super()._draw_invoice_from(canvas)
|
super()._draw_invoice_from(canvas)
|
||||||
@@ -859,8 +908,12 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
|||||||
|
|
||||||
def _get_first_page_frames(self, doc):
|
def _get_first_page_frames(self, doc):
|
||||||
footer_length = 3.5 * len(self.invoice.footer_text.split('\n')) * mm
|
footer_length = 3.5 * len(self.invoice.footer_text.split('\n')) * mm
|
||||||
|
if self.event.settings.invoice_renderer_highlight_order_code:
|
||||||
|
margin_top = 100 * mm
|
||||||
|
else:
|
||||||
|
margin_top = 95 * mm
|
||||||
return [
|
return [
|
||||||
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 95 * mm,
|
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - margin_top,
|
||||||
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
|
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
|
||||||
id='normal')
|
id='normal')
|
||||||
]
|
]
|
||||||
@@ -871,25 +924,35 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
|||||||
# the font size until it fits.
|
# the font size until it fits.
|
||||||
begin_top = 100 * mm
|
begin_top = 100 * mm
|
||||||
|
|
||||||
def _draw(label, value, value_size, x, width):
|
def _draw(label, value, value_size, x, width, bold=False, sublabel=None):
|
||||||
if canvas.stringWidth(value, self.font_regular, value_size) > width and value_size > 6:
|
if canvas.stringWidth(value, self.font_regular, value_size) > width and value_size > 6:
|
||||||
return False
|
return False
|
||||||
textobject = canvas.beginText(x, self.pagesize[1] - begin_top)
|
textobject = canvas.beginText(x, self.pagesize[1] - begin_top)
|
||||||
textobject.setFont(self.font_regular, 8)
|
textobject.setFont(self.font_regular, 8)
|
||||||
textobject.textLine(label)
|
textobject.textLine(self._normalize(label))
|
||||||
textobject.moveCursor(0, 5)
|
textobject.moveCursor(0, 5)
|
||||||
textobject.setFont(self.font_regular, value_size)
|
textobject.setFont(self.font_bold if bold else self.font_regular, value_size)
|
||||||
textobject.textLine(value)
|
textobject.textLine(self._normalize(value))
|
||||||
|
|
||||||
|
if sublabel:
|
||||||
|
textobject.moveCursor(0, 1)
|
||||||
|
textobject.setFont(self.font_regular, 8)
|
||||||
|
textobject.textLine(self._normalize(sublabel))
|
||||||
|
|
||||||
return textobject
|
return textobject
|
||||||
|
|
||||||
value_size = 10
|
value_size = 10
|
||||||
while value_size >= 5:
|
while value_size >= 5:
|
||||||
|
if self.event.settings.invoice_renderer_highlight_order_code:
|
||||||
|
kwargs = dict(bold=True, sublabel=pgettext('invoice', '(Please quote at all times.)'))
|
||||||
|
else:
|
||||||
|
kwargs = {}
|
||||||
objects = [
|
objects = [
|
||||||
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm)
|
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
|
||||||
]
|
]
|
||||||
|
|
||||||
p = Paragraph(
|
p = Paragraph(
|
||||||
date_format(self.invoice.date, "DATE_FORMAT"),
|
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
|
||||||
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
|
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
|
||||||
)
|
)
|
||||||
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
|
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
|
||||||
@@ -920,9 +983,9 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
|||||||
textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top)
|
textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top)
|
||||||
textobject.setFont(self.font_regular, 8)
|
textobject.setFont(self.font_regular, 8)
|
||||||
if self.invoice.is_cancellation:
|
if self.invoice.is_cancellation:
|
||||||
textobject.textLine(pgettext('invoice', 'Cancellation date'))
|
textobject.textLine(self._normalize(pgettext('invoice', 'Cancellation date')))
|
||||||
else:
|
else:
|
||||||
textobject.textLine(pgettext('invoice', 'Invoice date'))
|
textobject.textLine(self._normalize(pgettext('invoice', 'Invoice date')))
|
||||||
canvas.drawText(textobject)
|
canvas.drawText(textobject)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class Command(BaseCommand):
|
|||||||
del options['skip_checks']
|
del options['skip_checks']
|
||||||
del options['print_sql']
|
del options['print_sql']
|
||||||
|
|
||||||
if options['print_sql']:
|
if options.get('print_sql'):
|
||||||
connection.force_debug_cursor = True
|
connection.force_debug_cursor = True
|
||||||
logger = logging.getLogger("django.db.backends")
|
logger = logging.getLogger("django.db.backends")
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.2.18 on 2023-05-04 12:19
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0237_question_valid_string_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='giftcard',
|
||||||
|
name='owner_ticket',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='owned_gift_cards', to='pretixbase.orderposition'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 3.2.18 on 2023-05-11 11:02
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0238_giftcard_owner_ticket'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='giftcardtransaction',
|
||||||
|
name='acceptor',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='gift_card_transactions', to='pretixbase.organizer'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='giftcardtransaction',
|
||||||
|
name='info',
|
||||||
|
field=models.JSONField(null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.18 on 2023-05-16 11:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0239_giftcard_info'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='voucher',
|
||||||
|
name='all_addons_included',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='voucher',
|
||||||
|
name='all_bundles_included',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-05-25 15:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0240_auto_20230516_1119'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='itemmetaproperty',
|
||||||
|
name='allowed_values',
|
||||||
|
field=models.TextField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='itemmetaproperty',
|
||||||
|
name='required',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -178,7 +178,7 @@ class LoggedModel(models.Model, LoggingMixin):
|
|||||||
|
|
||||||
return LogEntry.objects.filter(
|
return LogEntry.objects.filter(
|
||||||
content_type=self.logs_content_type, object_id=self.pk
|
content_type=self.logs_content_type, object_id=self.pk
|
||||||
).select_related('user', 'event', 'oauth_application', 'api_token', 'device')
|
).select_related('user', 'event', 'event__organizer', 'oauth_application', 'api_token', 'device')
|
||||||
|
|
||||||
|
|
||||||
class LockModel:
|
class LockModel:
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ from pretix.base.models.fields import MultiStringField
|
|||||||
from pretix.base.models.organizer import Organizer
|
from pretix.base.models.organizer import Organizer
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.helpers.countries import FastCountryField
|
from pretix.helpers.countries import FastCountryField
|
||||||
|
from pretix.helpers.names import build_name
|
||||||
|
|
||||||
|
|
||||||
class CustomerSSOProvider(LoggedModel):
|
class CustomerSSOProvider(LoggedModel):
|
||||||
@@ -171,15 +172,11 @@ class Customer(LoggedModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
if not self.name_parts:
|
return build_name(self.name_parts, fallback_scheme=lambda: self.organizer.settings.name_scheme) or ""
|
||||||
return ""
|
|
||||||
if '_legacy' in self.name_parts:
|
@property
|
||||||
return self.name_parts['_legacy']
|
def name_all_components(self):
|
||||||
if '_scheme' in self.name_parts:
|
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.organizer.settings.name_scheme) or ""
|
||||||
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
|
|
||||||
else:
|
|
||||||
raise TypeError("Invalid name given.")
|
|
||||||
return scheme['concatenation'](self.name_parts).strip()
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
s = f'#{self.identifier}'
|
s = f'#{self.identifier}'
|
||||||
@@ -302,15 +299,11 @@ class AttendeeProfile(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def attendee_name(self):
|
def attendee_name(self):
|
||||||
if not self.attendee_name_parts:
|
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
|
||||||
return None
|
|
||||||
if '_legacy' in self.attendee_name_parts:
|
@property
|
||||||
return self.attendee_name_parts['_legacy']
|
def attendee_name_all_components(self):
|
||||||
if '_scheme' in self.attendee_name_parts:
|
return build_name(self.attendee_name_parts, "concatenation_all_components", fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
|
||||||
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
|
|
||||||
else:
|
|
||||||
scheme = PERSON_NAME_SCHEMES[self.customer.organizer.settings.name_scheme]
|
|
||||||
return scheme['concatenation'](self.attendee_name_parts).strip()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_name(self):
|
def state_name(self):
|
||||||
|
|||||||
@@ -290,19 +290,19 @@ class EventMixin:
|
|||||||
return safe_string(json.dumps(eventdict))
|
return safe_string(json.dumps(eventdict))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def annotated(cls, qs, channel='web'):
|
def annotated(cls, qs, channel='web', voucher=None):
|
||||||
from pretix.base.models import Item, ItemVariation, Quota
|
from pretix.base.models import Item, ItemVariation, Quota
|
||||||
|
|
||||||
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel).filter(
|
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
|
||||||
Q(variations__isnull=True)
|
Q(variations__isnull=True)
|
||||||
& Q(quotas__pk=OuterRef('pk'))
|
& Q(quotas__pk=OuterRef('pk'))
|
||||||
).order_by().values_list('quotas__pk').annotate(
|
).order_by().values_list('quotas__pk').annotate(
|
||||||
items=GroupConcat('pk', delimiter=',')
|
items=GroupConcat('pk', delimiter=',')
|
||||||
).values('items')
|
).values('items')
|
||||||
sq_active_variation = ItemVariation.objects.filter(
|
|
||||||
|
q_variation = (
|
||||||
Q(active=True)
|
Q(active=True)
|
||||||
& Q(sales_channels__contains=channel)
|
& Q(sales_channels__contains=channel)
|
||||||
& Q(hide_without_voucher=False)
|
|
||||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
||||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
||||||
& Q(item__active=True)
|
& Q(item__active=True)
|
||||||
@@ -310,10 +310,23 @@ class EventMixin:
|
|||||||
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now()))
|
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now()))
|
||||||
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
|
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
|
||||||
& Q(item__sales_channels__contains=channel)
|
& Q(item__sales_channels__contains=channel)
|
||||||
& Q(item__hide_without_voucher=False)
|
|
||||||
& Q(item__require_bundling=False)
|
& Q(item__require_bundling=False)
|
||||||
& Q(quotas__pk=OuterRef('pk'))
|
& Q(quotas__pk=OuterRef('pk'))
|
||||||
).order_by().values_list('quotas__pk').annotate(
|
)
|
||||||
|
|
||||||
|
if voucher:
|
||||||
|
if voucher.variation_id:
|
||||||
|
q_variation &= Q(pk=voucher.variation_id)
|
||||||
|
elif voucher.item_id:
|
||||||
|
q_variation &= Q(item_id=voucher.item_id)
|
||||||
|
elif voucher.quota_id:
|
||||||
|
q_variation &= Q(quotas__in=[voucher.quota_id])
|
||||||
|
|
||||||
|
if not voucher or not voucher.show_hidden_items:
|
||||||
|
q_variation &= Q(hide_without_voucher=False)
|
||||||
|
q_variation &= Q(item__hide_without_voucher=False)
|
||||||
|
|
||||||
|
sq_active_variation = ItemVariation.objects.filter(q_variation).order_by().values_list('quotas__pk').annotate(
|
||||||
items=GroupConcat('pk', delimiter=',')
|
items=GroupConcat('pk', delimiter=',')
|
||||||
).values('items')
|
).values('items')
|
||||||
quota_base_qs = Quota.objects.using(settings.DATABASE_REPLICA).filter(
|
quota_base_qs = Quota.objects.using(settings.DATABASE_REPLICA).filter(
|
||||||
@@ -625,6 +638,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
"""
|
"""
|
||||||
self.settings.invoice_renderer = 'modern1'
|
self.settings.invoice_renderer = 'modern1'
|
||||||
self.settings.invoice_include_expire_date = True
|
self.settings.invoice_include_expire_date = True
|
||||||
|
self.settings.invoice_renderer_highlight_order_code = True
|
||||||
self.settings.ticketoutput_pdf__enabled = True
|
self.settings.ticketoutput_pdf__enabled = True
|
||||||
self.settings.ticketoutput_passbook__enabled = True
|
self.settings.ticketoutput_passbook__enabled = True
|
||||||
self.settings.event_list_type = 'calendar'
|
self.settings.event_list_type = 'calendar'
|
||||||
@@ -1132,8 +1146,8 @@ class Event(EventMixin, LoggedModel):
|
|||||||
irs = self.get_invoice_renderers()
|
irs = self.get_invoice_renderers()
|
||||||
return irs[self.settings.invoice_renderer]
|
return irs[self.settings.invoice_renderer]
|
||||||
|
|
||||||
def subevents_annotated(self, channel):
|
def subevents_annotated(self, channel, voucher=None):
|
||||||
return SubEvent.annotated(self.subevents, channel)
|
return SubEvent.annotated(self.subevents, channel, voucher)
|
||||||
|
|
||||||
def subevents_sorted(self, queryset):
|
def subevents_sorted(self, queryset):
|
||||||
ordering = self.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
|
ordering = self.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
|
||||||
@@ -1453,10 +1467,10 @@ class SubEvent(EventMixin, LoggedModel):
|
|||||||
return qs_annotated
|
return qs_annotated
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def annotated(cls, qs, channel='web'):
|
def annotated(cls, qs, channel='web', voucher=None):
|
||||||
from .items import SubEventItem, SubEventItemVariation
|
from .items import SubEventItem, SubEventItemVariation
|
||||||
|
|
||||||
qs = super().annotated(qs, channel)
|
qs = super().annotated(qs, channel, voucher=voucher)
|
||||||
qs = qs.annotate(
|
qs = qs.annotate(
|
||||||
disabled_items=Coalesce(
|
disabled_items=Coalesce(
|
||||||
Subquery(
|
Subquery(
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ from django.conf import settings
|
|||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.html import format_html
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
@@ -66,6 +68,13 @@ class GiftCard(LoggedModel):
|
|||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
|
owner_ticket = models.ForeignKey(
|
||||||
|
'OrderPosition',
|
||||||
|
related_name='owned_gift_cards',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True, blank=True,
|
||||||
|
verbose_name=_('Owned by ticket holder')
|
||||||
|
)
|
||||||
issuance = models.DateTimeField(
|
issuance = models.DateTimeField(
|
||||||
auto_now_add=True,
|
auto_now_add=True,
|
||||||
)
|
)
|
||||||
@@ -153,6 +162,61 @@ class GiftCardTransaction(models.Model):
|
|||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
text = models.TextField(blank=True, null=True)
|
text = models.TextField(blank=True, null=True)
|
||||||
|
info = models.JSONField(
|
||||||
|
null=True, blank=True,
|
||||||
|
)
|
||||||
|
acceptor = models.ForeignKey(
|
||||||
|
'Organizer',
|
||||||
|
related_name='gift_card_transactions',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
null=True, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("datetime",)
|
ordering = ("datetime",)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.pk and not self.acceptor:
|
||||||
|
raise ValueError("`acceptor` should be set on all new gift card transactions.")
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def display(self, customer_facing=True):
|
||||||
|
from ..signals import gift_card_transaction_display
|
||||||
|
|
||||||
|
for receiver, response in gift_card_transaction_display.send(self, transaction=self, customer_facing=customer_facing):
|
||||||
|
if response:
|
||||||
|
return response
|
||||||
|
|
||||||
|
if self.order_id:
|
||||||
|
if not self.text:
|
||||||
|
if not customer_facing:
|
||||||
|
return format_html(
|
||||||
|
'<a href="{}">{}</a>',
|
||||||
|
reverse(
|
||||||
|
"control:event.order",
|
||||||
|
kwargs={
|
||||||
|
"event": self.order.event.slug,
|
||||||
|
"organizer": self.order.event.organizer.slug,
|
||||||
|
"code": self.order.code,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
self.order.full_code
|
||||||
|
)
|
||||||
|
return self.order.full_code
|
||||||
|
else:
|
||||||
|
return self.text
|
||||||
|
else:
|
||||||
|
if self.text:
|
||||||
|
return format_html(
|
||||||
|
'<em>{}:</em> {}',
|
||||||
|
_('Manual transaction'),
|
||||||
|
self.text,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return _('Manual transaction')
|
||||||
|
|
||||||
|
def display_backend(self):
|
||||||
|
return self.display(customer_facing=False)
|
||||||
|
|
||||||
|
def display_presale(self):
|
||||||
|
return self.display(customer_facing=True)
|
||||||
|
|||||||
@@ -2001,6 +2001,15 @@ class ItemMetaProperty(LoggedModel):
|
|||||||
verbose_name=_("Name"),
|
verbose_name=_("Name"),
|
||||||
)
|
)
|
||||||
default = models.TextField(blank=True)
|
default = models.TextField(blank=True)
|
||||||
|
required = models.BooleanField(
|
||||||
|
default=False, verbose_name=_("Required for products"),
|
||||||
|
help_text=_("If checked, this property must be set in each product. Does not apply if a default value is set.")
|
||||||
|
)
|
||||||
|
allowed_values = models.TextField(
|
||||||
|
null=True, blank=True,
|
||||||
|
verbose_name=_("Valid values"),
|
||||||
|
help_text=_("If you keep this empty, any value is allowed. Otherwise, enter one possible value per line.")
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from i18nfield.fields import I18nCharField
|
|||||||
from pretix.base.models import Customer
|
from pretix.base.models import Customer
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.models.organizer import Organizer
|
from pretix.base.models.organizer import Organizer
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.helpers.names import build_name
|
||||||
|
|
||||||
|
|
||||||
class MembershipType(LoggedModel):
|
class MembershipType(LoggedModel):
|
||||||
@@ -160,15 +160,7 @@ class Membership(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def attendee_name(self):
|
def attendee_name(self):
|
||||||
if not self.attendee_name_parts:
|
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
|
||||||
return None
|
|
||||||
if '_legacy' in self.attendee_name_parts:
|
|
||||||
return self.attendee_name_parts['_legacy']
|
|
||||||
if '_scheme' in self.attendee_name_parts:
|
|
||||||
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
|
|
||||||
else:
|
|
||||||
scheme = PERSON_NAME_SCHEMES[self.customer.organizer.settings.name_scheme]
|
|
||||||
return scheme['concatenation'](self.attendee_name_parts).strip()
|
|
||||||
|
|
||||||
def is_valid(self, ev=None):
|
def is_valid(self, ev=None):
|
||||||
if ev:
|
if ev:
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ from pretix.base.signals import order_gracefully_delete
|
|||||||
from ...helpers import OF_SELF
|
from ...helpers import OF_SELF
|
||||||
from ...helpers.countries import CachedCountries, FastCountryField
|
from ...helpers.countries import CachedCountries, FastCountryField
|
||||||
from ...helpers.format import format_map
|
from ...helpers.format import format_map
|
||||||
|
from ...helpers.names import build_name
|
||||||
from ._transactions import (
|
from ._transactions import (
|
||||||
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
|
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
|
||||||
)
|
)
|
||||||
@@ -1451,15 +1452,11 @@ class AbstractPosition(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def attendee_name(self):
|
def attendee_name(self):
|
||||||
if not self.attendee_name_parts:
|
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.event.settings.name_scheme)
|
||||||
return None
|
|
||||||
if '_legacy' in self.attendee_name_parts:
|
@property
|
||||||
return self.attendee_name_parts['_legacy']
|
def attendee_name_all_components(self):
|
||||||
if '_scheme' in self.attendee_name_parts:
|
return build_name(self.attendee_name_parts, "concatenation_all_components", fallback_scheme=lambda: self.event.settings.name_scheme)
|
||||||
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
|
|
||||||
else:
|
|
||||||
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
|
|
||||||
return scheme['concatenation'](self.attendee_name_parts).strip()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_name(self):
|
def state_name(self):
|
||||||
@@ -2834,8 +2831,12 @@ class CartPosition(AbstractPosition):
|
|||||||
if self.is_bundled:
|
if self.is_bundled:
|
||||||
bundle = self.addon_to.item.bundles.filter(bundled_item=self.item, bundled_variation=self.variation).first()
|
bundle = self.addon_to.item.bundles.filter(bundled_item=self.item, bundled_variation=self.variation).first()
|
||||||
if bundle:
|
if bundle:
|
||||||
listed_price = bundle.designated_price
|
if self.addon_to.voucher_id and self.addon_to.voucher.all_bundles_included:
|
||||||
price_after_voucher = bundle.designated_price
|
listed_price = Decimal('0.00')
|
||||||
|
price_after_voucher = Decimal('0.00')
|
||||||
|
else:
|
||||||
|
listed_price = bundle.designated_price
|
||||||
|
price_after_voucher = bundle.designated_price
|
||||||
|
|
||||||
if listed_price != self.listed_price or price_after_voucher != self.price_after_voucher:
|
if listed_price != self.listed_price or price_after_voucher != self.price_after_voucher:
|
||||||
self.listed_price = listed_price
|
self.listed_price = listed_price
|
||||||
@@ -2980,15 +2981,11 @@ class InvoiceAddress(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
if not self.name_parts:
|
return build_name(self.name_parts, fallback_scheme=lambda: self.order.event.settings.name_scheme) or ""
|
||||||
return ""
|
|
||||||
if '_legacy' in self.name_parts:
|
@property
|
||||||
return self.name_parts['_legacy']
|
def name_all_components(self):
|
||||||
if '_scheme' in self.name_parts:
|
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.order.event.settings.name_scheme) or ""
|
||||||
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
|
|
||||||
else:
|
|
||||||
raise TypeError("Invalid name given.")
|
|
||||||
return scheme['concatenation'](self.name_parts).strip()
|
|
||||||
|
|
||||||
def for_js(self):
|
def for_js(self):
|
||||||
d = {}
|
d = {}
|
||||||
|
|||||||
@@ -296,6 +296,14 @@ class Voucher(LoggedModel):
|
|||||||
verbose_name=_("Shows hidden products that match this voucher"),
|
verbose_name=_("Shows hidden products that match this voucher"),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
all_addons_included = models.BooleanField(
|
||||||
|
verbose_name=_("Offer all add-on products for free when redeeming this voucher"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
all_bundles_included = models.BooleanField(
|
||||||
|
verbose_name=_("Include all bundled products without a designated price when redeeming this voucher"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='event__organizer')
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ from pretix.base.email import get_email_context
|
|||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import User, Voucher
|
from pretix.base.models import User, Voucher
|
||||||
from pretix.base.services.mail import SendMailException, mail, render_mail
|
from pretix.base.services.mail import SendMailException, mail, render_mail
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
|
||||||
|
|
||||||
from ...helpers.format import format_map
|
from ...helpers.format import format_map
|
||||||
|
from ...helpers.names import build_name
|
||||||
from .base import LoggedModel
|
from .base import LoggedModel
|
||||||
from .event import Event, SubEvent
|
from .event import Event, SubEvent
|
||||||
from .items import Item, ItemVariation
|
from .items import Item, ItemVariation
|
||||||
@@ -119,7 +119,7 @@ class WaitingListEntry(LoggedModel):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
try:
|
try:
|
||||||
WaitingListEntry.clean_duplicate(self.email, self.item, self.variation, self.subevent, self.pk)
|
WaitingListEntry.clean_duplicate(self.event, self.email, self.item, self.variation, self.subevent, self.pk)
|
||||||
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
|
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
|
||||||
WaitingListEntry.clean_subevent(self.event, self.subevent)
|
WaitingListEntry.clean_subevent(self.event, self.subevent)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
@@ -136,15 +136,11 @@ class WaitingListEntry(LoggedModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
if not self.name_parts:
|
return build_name(self.name_parts, fallback_scheme=lambda: self.event.settings.name_scheme)
|
||||||
return None
|
|
||||||
if '_legacy' in self.name_parts:
|
@property
|
||||||
return self.name_parts['_legacy']
|
def name_all_components(self):
|
||||||
if '_scheme' in self.name_parts:
|
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.event.settings.name_scheme)
|
||||||
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
|
|
||||||
else:
|
|
||||||
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
|
|
||||||
return scheme['concatenation'](self.name_parts).strip()
|
|
||||||
|
|
||||||
def send_voucher(self, quota_cache=None, user=None, auth=None):
|
def send_voucher(self, quota_cache=None, user=None, auth=None):
|
||||||
availability = (
|
availability = (
|
||||||
@@ -308,9 +304,9 @@ class WaitingListEntry(LoggedModel):
|
|||||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_duplicate(email, item, variation, subevent, pk):
|
def clean_duplicate(event, email, item, variation, subevent, pk):
|
||||||
if WaitingListEntry.objects.filter(
|
if WaitingListEntry.objects.filter(
|
||||||
item=item, variation=variation, email__iexact=email, voucher__isnull=True, subevent=subevent
|
item=item, variation=variation, email__iexact=email, voucher__isnull=True, subevent=subevent
|
||||||
).exclude(pk=pk).exists():
|
).exclude(pk=pk).count() >= event.settings.waiting_list_limit_per_user:
|
||||||
raise ValidationError(_('You are already on this waiting list! We will notify '
|
raise ValidationError(_('You are already on this waiting list! We will notify '
|
||||||
'you as soon as we have a ticket available for you.'))
|
'you as soon as we have a ticket available for you.'))
|
||||||
|
|||||||
@@ -1469,7 +1469,8 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
trans = gc.transactions.create(
|
trans = gc.transactions.create(
|
||||||
value=-1 * payment.amount,
|
value=-1 * payment.amount,
|
||||||
order=payment.order,
|
order=payment.order,
|
||||||
payment=payment
|
payment=payment,
|
||||||
|
acceptor=self.event.organizer,
|
||||||
)
|
)
|
||||||
payment.info_data = {
|
payment.info_data = {
|
||||||
'gift_card': gc.pk,
|
'gift_card': gc.pk,
|
||||||
@@ -1490,7 +1491,8 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
trans = gc.transactions.create(
|
trans = gc.transactions.create(
|
||||||
value=refund.amount,
|
value=refund.amount,
|
||||||
order=refund.order,
|
order=refund.order,
|
||||||
refund=refund
|
refund=refund,
|
||||||
|
acceptor=self.event.organizer,
|
||||||
)
|
)
|
||||||
refund.info_data = {
|
refund.info_data = {
|
||||||
'gift_card': gc.pk,
|
'gift_card': gc.pk,
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ from functools import partial
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import jsonschema
|
import jsonschema
|
||||||
from arabic_reshaper import ArabicReshaper
|
|
||||||
from bidi.algorithm import get_display
|
from bidi.algorithm import get_display
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
@@ -57,7 +56,6 @@ from django.db.models import Max, Min
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import SimpleLazyObject
|
|
||||||
from django.utils.html import conditional_escape
|
from django.utils.html import conditional_escape
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext
|
from django.utils.translation import gettext_lazy as _, pgettext
|
||||||
@@ -78,12 +76,12 @@ from reportlab.pdfgen.canvas import Canvas
|
|||||||
from reportlab.platypus import Paragraph
|
from reportlab.platypus import Paragraph
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.invoice import ThumbnailingImageReader
|
|
||||||
from pretix.base.models import Order, OrderPosition, Question
|
from pretix.base.models import Order, OrderPosition, Question
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.base.signals import layout_image_variables, layout_text_variables
|
from pretix.base.signals import layout_image_variables, layout_text_variables
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.base.templatetags.phone_format import phone_format
|
from pretix.base.templatetags.phone_format import phone_format
|
||||||
|
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -699,12 +697,6 @@ def get_seat(op: OrderPosition):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
|
||||||
'delete_harakat': True,
|
|
||||||
'support_ligatures': False,
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
|
||||||
class Renderer:
|
class Renderer:
|
||||||
|
|
||||||
def __init__(self, event, layout, background_file):
|
def __init__(self, event, layout, background_file):
|
||||||
|
|||||||
@@ -512,7 +512,10 @@ class CartManager:
|
|||||||
if cp.is_bundled:
|
if cp.is_bundled:
|
||||||
bundle = cp.addon_to.item.bundles.filter(bundled_item=cp.item, bundled_variation=cp.variation).first()
|
bundle = cp.addon_to.item.bundles.filter(bundled_item=cp.item, bundled_variation=cp.variation).first()
|
||||||
if bundle:
|
if bundle:
|
||||||
listed_price = bundle.designated_price or Decimal('0.00')
|
if cp.addon_to.voucher_id and cp.addon_to.voucher.all_bundles_included:
|
||||||
|
listed_price = Decimal('0.00')
|
||||||
|
else:
|
||||||
|
listed_price = bundle.designated_price
|
||||||
else:
|
else:
|
||||||
listed_price = cp.price
|
listed_price = cp.price
|
||||||
price_after_voucher = listed_price
|
price_after_voucher = listed_price
|
||||||
@@ -712,6 +715,11 @@ class CartManager:
|
|||||||
else:
|
else:
|
||||||
bundle_quotas = []
|
bundle_quotas = []
|
||||||
|
|
||||||
|
if voucher and voucher.all_bundles_included:
|
||||||
|
bundled_price = Decimal('0.00')
|
||||||
|
else:
|
||||||
|
bundled_price = bundle.designated_price
|
||||||
|
|
||||||
bop = self.AddOperation(
|
bop = self.AddOperation(
|
||||||
count=bundle.count,
|
count=bundle.count,
|
||||||
item=bitem,
|
item=bitem,
|
||||||
@@ -722,8 +730,8 @@ class CartManager:
|
|||||||
subevent=subevent,
|
subevent=subevent,
|
||||||
bundled=[],
|
bundled=[],
|
||||||
seat=None,
|
seat=None,
|
||||||
listed_price=bundle.designated_price,
|
listed_price=bundled_price,
|
||||||
price_after_voucher=bundle.designated_price,
|
price_after_voucher=bundled_price,
|
||||||
custom_price_input=None,
|
custom_price_input=None,
|
||||||
custom_price_input_is_net=False,
|
custom_price_input_is_net=False,
|
||||||
voucher_ignored=False,
|
voucher_ignored=False,
|
||||||
@@ -809,7 +817,6 @@ class CartManager:
|
|||||||
quota_diff = Counter() # Quota -> Number of usages
|
quota_diff = Counter() # Quota -> Number of usages
|
||||||
operations = []
|
operations = []
|
||||||
available_categories = defaultdict(set) # CartPos -> Category IDs to choose from
|
available_categories = defaultdict(set) # CartPos -> Category IDs to choose from
|
||||||
price_included = defaultdict(dict) # CartPos -> CategoryID -> bool(price is included)
|
|
||||||
toplevel_cp = self.positions.filter(
|
toplevel_cp = self.positions.filter(
|
||||||
addon_to__isnull=True
|
addon_to__isnull=True
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
@@ -819,7 +826,6 @@ class CartManager:
|
|||||||
# Prefill some of the cache containers
|
# Prefill some of the cache containers
|
||||||
for cp in toplevel_cp:
|
for cp in toplevel_cp:
|
||||||
available_categories[cp.pk] = {iao.addon_category_id for iao in cp.item.addons.all()}
|
available_categories[cp.pk] = {iao.addon_category_id for iao in cp.item.addons.all()}
|
||||||
price_included[cp.pk] = {iao.addon_category_id: iao.price_included for iao in cp.item.addons.all()}
|
|
||||||
cpcache[cp.pk] = cp
|
cpcache[cp.pk] = cp
|
||||||
for a in cp.addons.all():
|
for a in cp.addons.all():
|
||||||
if not a.is_bundled:
|
if not a.is_bundled:
|
||||||
|
|||||||
@@ -510,7 +510,7 @@ def send_invoices_to_organizer(sender, **kwargs):
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
qs = Invoice.objects.filter(
|
qs = Invoice.objects.filter(
|
||||||
sent_to_organizer__isnull=True
|
sent_to_organizer__isnull=True
|
||||||
).prefetch_related('event').select_for_update(of=OF_SELF, skip_locked=connection.features.has_select_for_update_skip_locked)
|
).prefetch_related('event', 'order').select_for_update(of=OF_SELF, skip_locked=connection.features.has_select_for_update_skip_locked)
|
||||||
for i in qs[:batch_size]:
|
for i in qs[:batch_size]:
|
||||||
if i.event.settings.invoice_email_organizer:
|
if i.event.settings.invoice_email_organizer:
|
||||||
with language(i.event.settings.locale):
|
with language(i.event.settings.locale):
|
||||||
@@ -519,11 +519,12 @@ def send_invoices_to_organizer(sender, **kwargs):
|
|||||||
subject=_('New invoice: {number}').format(number=i.number),
|
subject=_('New invoice: {number}').format(number=i.number),
|
||||||
template=LazyI18nString.from_gettext(_(
|
template=LazyI18nString.from_gettext(_(
|
||||||
'Hello,\n\n'
|
'Hello,\n\n'
|
||||||
'a new invoice for {event} has been created, see attached.\n\n'
|
'a new invoice for order {order} at {event} has been created, see attached.\n\n'
|
||||||
'We are sending this email because you configured us to do so in your event settings.'
|
'We are sending this email because you configured us to do so in your event settings.'
|
||||||
)),
|
)),
|
||||||
context={
|
context={
|
||||||
'event': str(i.event),
|
'event': str(i.event),
|
||||||
|
'order': str(i.order),
|
||||||
},
|
},
|
||||||
locale=i.event.settings.locale,
|
locale=i.event.settings.locale,
|
||||||
event=i.event,
|
event=i.event,
|
||||||
|
|||||||
@@ -466,9 +466,17 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
for inv in invoices:
|
for inv in invoices:
|
||||||
if inv.file:
|
if inv.file:
|
||||||
try:
|
try:
|
||||||
|
# We try to give the invoice a more human-readable name, e.g. "Invoice_ABC-123.pdf" instead of
|
||||||
|
# just "ABC-123.pdf", but we only do so if our currently selected language allows to do this
|
||||||
|
# as ASCII text. For example, we would not want a "فاتورة_" prefix for our filename since this
|
||||||
|
# has shown to cause deliverability problems of the email and deliverability wins.
|
||||||
|
filename = pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf'
|
||||||
|
if not re.match("^[a-zA-Z0-9-_%./,&:# ]+$", filename):
|
||||||
|
filename = inv.number.replace(' ', '_') + '.pdf'
|
||||||
|
filename = re.sub("[^a-zA-Z0-9-_.]+", "_", filename)
|
||||||
with language(inv.order.locale):
|
with language(inv.order.locale):
|
||||||
email.attach(
|
email.attach(
|
||||||
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
|
filename,
|
||||||
inv.file.file.read(),
|
inv.file.file.read(),
|
||||||
'application/pdf'
|
'application/pdf'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
|
|||||||
from pretix.base.signals import (
|
from pretix.base.signals import (
|
||||||
allow_ticket_download, order_approved, order_canceled, order_changed,
|
allow_ticket_download, order_approved, order_canceled, order_changed,
|
||||||
order_denied, order_expired, order_fee_calculation, order_paid,
|
order_denied, order_expired, order_fee_calculation, order_paid,
|
||||||
order_placed, order_split, periodic_task, validate_order,
|
order_placed, order_split, order_valid_if_pending, periodic_task,
|
||||||
|
validate_order,
|
||||||
)
|
)
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
@@ -235,7 +236,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
|
|||||||
|
|
||||||
for gc in position.issued_gift_cards.all():
|
for gc in position.issued_gift_cards.all():
|
||||||
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
|
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
|
||||||
gc.transactions.create(value=position.price, order=order)
|
gc.transactions.create(value=position.price, order=order, acceptor=order.event.organizer)
|
||||||
break
|
break
|
||||||
|
|
||||||
for m in position.granted_memberships.all():
|
for m in position.granted_memberships.all():
|
||||||
@@ -513,7 +514,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
gc.transactions.create(value=-position.price, order=order)
|
gc.transactions.create(value=-position.price, order=order, acceptor=order.event.organizer)
|
||||||
|
|
||||||
for m in position.granted_memberships.all():
|
for m in position.granted_memberships.all():
|
||||||
m.canceled = True
|
m.canceled = True
|
||||||
@@ -922,7 +923,7 @@ def _get_fees(positions: List[CartPosition], payment_requests: List[dict], addre
|
|||||||
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
|
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
|
||||||
payment_requests: List[dict], locale: str=None, address: InvoiceAddress=None,
|
payment_requests: List[dict], locale: str=None, address: InvoiceAddress=None,
|
||||||
meta_info: dict=None, sales_channel: str='web', shown_total=None,
|
meta_info: dict=None, sales_channel: str='web', shown_total=None,
|
||||||
customer=None):
|
customer=None, valid_if_pending=False):
|
||||||
payments = []
|
payments = []
|
||||||
sales_channel = get_all_sales_channels()[sales_channel]
|
sales_channel = get_all_sales_channels()[sales_channel]
|
||||||
|
|
||||||
@@ -950,6 +951,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
|||||||
require_approval=require_approval,
|
require_approval=require_approval,
|
||||||
sales_channel=sales_channel.identifier,
|
sales_channel=sales_channel.identifier,
|
||||||
customer=customer,
|
customer=customer,
|
||||||
|
valid_if_pending=valid_if_pending,
|
||||||
)
|
)
|
||||||
if customer:
|
if customer:
|
||||||
order.email_known_to_work = customer.is_verified
|
order.email_known_to_work = customer.is_verified
|
||||||
@@ -1094,6 +1096,20 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
|||||||
customer=customer,
|
customer=customer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
valid_if_pending = False
|
||||||
|
for recv, result in order_valid_if_pending.send(
|
||||||
|
event,
|
||||||
|
payments=payment_requests,
|
||||||
|
email=email,
|
||||||
|
positions=positions,
|
||||||
|
locale=locale,
|
||||||
|
invoice_address=addr,
|
||||||
|
meta_info=meta_info,
|
||||||
|
customer=customer,
|
||||||
|
):
|
||||||
|
if result:
|
||||||
|
valid_if_pending = True
|
||||||
|
|
||||||
lockfn = NoLockManager
|
lockfn = NoLockManager
|
||||||
locked = False
|
locked = False
|
||||||
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2)) | Q(seat__isnull=False)).exists():
|
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2)) | Q(seat__isnull=False)).exists():
|
||||||
@@ -1117,7 +1133,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
|||||||
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer)
|
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer)
|
||||||
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests,
|
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests,
|
||||||
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
|
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
|
||||||
shown_total=shown_total, customer=customer)
|
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
|
||||||
try:
|
try:
|
||||||
for p in payment_objs:
|
for p in payment_objs:
|
||||||
if p.provider == 'free':
|
if p.provider == 'free':
|
||||||
@@ -1442,6 +1458,16 @@ class OrderChangeManager:
|
|||||||
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
|
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
|
||||||
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
|
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
|
||||||
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
|
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
|
||||||
|
'max_items_per_product': ngettext_lazy(
|
||||||
|
"You cannot select more than %(max)s item of the product %(product)s.",
|
||||||
|
"You cannot select more than %(max)s items of the product %(product)s.",
|
||||||
|
"max"
|
||||||
|
),
|
||||||
|
'min_items_per_product': ngettext_lazy(
|
||||||
|
"You need to select at least %(min)s item of the product %(product)s.",
|
||||||
|
"You need to select at least %(min)s items of the product %(product)s.",
|
||||||
|
"min"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
|
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
|
||||||
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
|
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
|
||||||
@@ -1744,6 +1770,11 @@ class OrderChangeManager:
|
|||||||
if self._operations:
|
if self._operations:
|
||||||
raise ValueError("Setting addons should be the first/only operation")
|
raise ValueError("Setting addons should be the first/only operation")
|
||||||
|
|
||||||
|
# Prepare containers for min/max check of products
|
||||||
|
item_counts = Counter()
|
||||||
|
for p in self.order.positions.all():
|
||||||
|
item_counts[p.item] += 1
|
||||||
|
|
||||||
# Prepare various containers to hold data later
|
# Prepare various containers to hold data later
|
||||||
current_addons = defaultdict(lambda: defaultdict(list)) # OrderPos -> currently attached add-ons
|
current_addons = defaultdict(lambda: defaultdict(list)) # OrderPos -> currently attached add-ons
|
||||||
input_addons = defaultdict(Counter) # OrderPos -> final desired set of add-ons
|
input_addons = defaultdict(Counter) # OrderPos -> final desired set of add-ons
|
||||||
@@ -1861,7 +1892,7 @@ class OrderChangeManager:
|
|||||||
input_addons[op.id][a['item'], a['variation']] = a.get('count', 1)
|
input_addons[op.id][a['item'], a['variation']] = a.get('count', 1)
|
||||||
selected_addons[op.id, item.category_id][a['item'], a['variation']] = a.get('count', 1)
|
selected_addons[op.id, item.category_id][a['item'], a['variation']] = a.get('count', 1)
|
||||||
|
|
||||||
if price_included[op.pk].get(item.category_id):
|
if price_included[op.pk].get(item.category_id) or (op.voucher_id and op.voucher.all_addons_included):
|
||||||
price = TAXED_ZERO
|
price = TAXED_ZERO
|
||||||
else:
|
else:
|
||||||
price = get_price(
|
price = get_price(
|
||||||
@@ -1880,6 +1911,7 @@ class OrderChangeManager:
|
|||||||
item=item, variation=variation, price=price,
|
item=item, variation=variation, price=price,
|
||||||
addon_to=op, subevent=op.subevent, seat=None,
|
addon_to=op, subevent=op.subevent, seat=None,
|
||||||
)
|
)
|
||||||
|
item_counts[item] += 1
|
||||||
|
|
||||||
# Check constraints on the add-on combinations
|
# Check constraints on the add-on combinations
|
||||||
for op in toplevel_op:
|
for op in toplevel_op:
|
||||||
@@ -1929,6 +1961,27 @@ class OrderChangeManager:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.cancel(a)
|
self.cancel(a)
|
||||||
|
item_counts[a.item] -= 1
|
||||||
|
|
||||||
|
for item, count in item_counts.items():
|
||||||
|
if count == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.max_per_order and count > item.max_per_order:
|
||||||
|
raise OrderError(
|
||||||
|
self.error_messages['max_items_per_product'] % {
|
||||||
|
'max': item.max_per_order,
|
||||||
|
'product': item.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if item.min_per_order and count < item.min_per_order:
|
||||||
|
raise OrderError(
|
||||||
|
self.error_messages['min_items_per_product'] % {
|
||||||
|
'min': item.min_per_order,
|
||||||
|
'product': item.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def _check_seats(self):
|
def _check_seats(self):
|
||||||
for seat, diff in self._seatdiff.items():
|
for seat, diff in self._seatdiff.items():
|
||||||
@@ -2186,7 +2239,7 @@ class OrderChangeManager:
|
|||||||
card=gc.secret
|
card=gc.secret
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
gc.transactions.create(value=-op.position.price, order=self.order)
|
gc.transactions.create(value=-op.position.price, order=self.order, acceptor=self.order.event.organizer)
|
||||||
|
|
||||||
for m in op.position.granted_memberships.with_usages().all():
|
for m in op.position.granted_memberships.with_usages().all():
|
||||||
m.canceled = True
|
m.canceled = True
|
||||||
@@ -2202,7 +2255,7 @@ class OrderChangeManager:
|
|||||||
card=gc.secret
|
card=gc.secret
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
gc.transactions.create(value=-opa.position.price, order=self.order)
|
gc.transactions.create(value=-opa.position.price, order=self.order, acceptor=self.order.event.organizer)
|
||||||
|
|
||||||
for m in opa.granted_memberships.with_usages().all():
|
for m in opa.granted_memberships.with_usages().all():
|
||||||
m.canceled = True
|
m.canceled = True
|
||||||
@@ -2918,7 +2971,7 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
|
|||||||
currency=sender.currency, issued_in=p, testmode=order.testmode,
|
currency=sender.currency, issued_in=p, testmode=order.testmode,
|
||||||
expires=sender.organizer.default_gift_card_expiry,
|
expires=sender.organizer.default_gift_card_expiry,
|
||||||
)
|
)
|
||||||
gc.transactions.create(value=p.price - issued, order=order)
|
gc.transactions.create(value=p.price - issued, order=order, acceptor=sender.organizer)
|
||||||
any_giftcards = True
|
any_giftcards = True
|
||||||
p.secret = gc.secret
|
p.secret = gc.secret
|
||||||
p.save(update_fields=['secret'])
|
p.save(update_fields=['secret'])
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ def is_included_for_free(item: Item, addon_to: AbstractPosition):
|
|||||||
return True
|
return True
|
||||||
except ItemAddOn.DoesNotExist:
|
except ItemAddOn.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
if addon_to.voucher_id and addon_to.voucher.all_addons_included:
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
|||||||
for ir, r in enumerate(recipients):
|
for ir, r in enumerate(recipients):
|
||||||
voucher_list = []
|
voucher_list = []
|
||||||
for i in range(r['number']):
|
for i in range(r['number']):
|
||||||
voucher_list.append(vouchers.pop())
|
voucher_list.append(vouchers.pop(0))
|
||||||
with language(event.settings.locale):
|
with language(event.settings.locale):
|
||||||
email_context = get_email_context(event=event, name=r.get('name') or '',
|
email_context = get_email_context(event=event, name=r.get('name') or '',
|
||||||
voucher_list=[v.code for v in voucher_list])
|
voucher_list=[v.code for v in voucher_list])
|
||||||
|
|||||||
@@ -96,6 +96,18 @@ def primary_font_kwargs():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def invoice_font_kwargs():
|
||||||
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
|
choices = [('Open Sans', 'Open Sans')]
|
||||||
|
choices += sorted([
|
||||||
|
(a, a) for a, v in get_fonts().items()
|
||||||
|
], key=lambda a: a[0])
|
||||||
|
return {
|
||||||
|
'choices': choices,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def restricted_plugin_kwargs():
|
def restricted_plugin_kwargs():
|
||||||
from pretix.base.plugins import get_all_plugins
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
@@ -621,6 +633,17 @@ DEFAULTS = {
|
|||||||
"used at most once over all of your events. This setting only affects future invoices. You can "
|
"used at most once over all of your events. This setting only affects future invoices. You can "
|
||||||
"use %Y (with century) %y (without century) to insert the year of the invoice, or %m and %d for "
|
"use %Y (with century) %y (without century) to insert the year of the invoice, or %m and %d for "
|
||||||
"the day of month."),
|
"the day of month."),
|
||||||
|
validators=[
|
||||||
|
RegexValidator(
|
||||||
|
# We actually allow more characters than we name in the error message since some of these characters
|
||||||
|
# are in active use at the time of the introduction of this validation, so we can't really forbid
|
||||||
|
# them, but we don't think they belong in an invoice number and don't want to advertise them.
|
||||||
|
regex="^[a-zA-Z0-9-_%./,&:# ]+$",
|
||||||
|
message=lazy(lambda *args: _('Please only use the characters {allowed} in this field.').format(
|
||||||
|
allowed='A-Z, a-z, 0-9, -./:#'
|
||||||
|
), str)()
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'invoice_numbers_prefix_cancellations': {
|
'invoice_numbers_prefix_cancellations': {
|
||||||
@@ -632,8 +655,42 @@ DEFAULTS = {
|
|||||||
label=_("Invoice number prefix for cancellations"),
|
label=_("Invoice number prefix for cancellations"),
|
||||||
help_text=_("This will be prepended to invoice numbers of cancellations. If you leave this field empty, "
|
help_text=_("This will be prepended to invoice numbers of cancellations. If you leave this field empty, "
|
||||||
"the same numbering scheme will be used that you configured for regular invoices."),
|
"the same numbering scheme will be used that you configured for regular invoices."),
|
||||||
|
validators=[
|
||||||
|
RegexValidator(
|
||||||
|
# We actually allow more characters than we name in the error message since some of these characters
|
||||||
|
# are in active use at the time of the introduction of this validation, so we can't really forbid
|
||||||
|
# them, but we don't think they belong in an invoice number and don't want to advertise them.
|
||||||
|
regex="^[a-zA-Z0-9-_%./,&:# ]+$",
|
||||||
|
message=lazy(lambda *args: _('Please only use the characters {allowed} in this field.').format(
|
||||||
|
allowed='A-Z, a-z, 0-9, -./:#'
|
||||||
|
), str)()
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'invoice_renderer_highlight_order_code': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool,
|
||||||
|
'form_class': forms.BooleanField,
|
||||||
|
'serializer_class': serializers.BooleanField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Highlight order code to make it stand out visibly"),
|
||||||
|
help_text=_("Only respected by some invoice renderers."),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'invoice_renderer_font': {
|
||||||
|
'default': 'Open Sans',
|
||||||
|
'type': str,
|
||||||
|
'form_class': forms.ChoiceField,
|
||||||
|
'serializer_class': serializers.ChoiceField,
|
||||||
|
'serializer_kwargs': lambda: dict(**invoice_font_kwargs()),
|
||||||
|
'form_kwargs': lambda: dict(
|
||||||
|
label=_('Font'),
|
||||||
|
help_text=_("Only respected by some invoice renderers."),
|
||||||
|
required=True,
|
||||||
|
**invoice_font_kwargs()
|
||||||
|
),
|
||||||
|
},
|
||||||
'invoice_renderer': {
|
'invoice_renderer': {
|
||||||
'default': 'classic', # default for new events is 'modern1'
|
'default': 'classic', # default for new events is 'modern1'
|
||||||
'type': str,
|
'type': str,
|
||||||
@@ -1330,6 +1387,21 @@ DEFAULTS = {
|
|||||||
help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.")
|
help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.")
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'waiting_list_limit_per_user': {
|
||||||
|
'default': '1',
|
||||||
|
'type': int,
|
||||||
|
'serializer_class': serializers.IntegerField,
|
||||||
|
'form_class': forms.IntegerField,
|
||||||
|
'serializer_kwargs': dict(
|
||||||
|
min_value=1,
|
||||||
|
),
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Maximum number of entries per email address for the same product"),
|
||||||
|
min_value=1,
|
||||||
|
required=True,
|
||||||
|
widget=forms.NumberInput(),
|
||||||
|
)
|
||||||
|
},
|
||||||
'show_checkin_number_user': {
|
'show_checkin_number_user': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
@@ -1337,7 +1409,7 @@ DEFAULTS = {
|
|||||||
'form_class': forms.BooleanField,
|
'form_class': forms.BooleanField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Show number of check-ins to customer"),
|
label=_("Show number of check-ins to customer"),
|
||||||
help_text=_('With this option enabled, your customers will be able how many times they entered '
|
help_text=_('With this option enabled, your customers will be able to see how many times they entered '
|
||||||
'the event. This is usually not necessary, but might be useful in combination with tickets '
|
'the event. This is usually not necessary, but might be useful in combination with tickets '
|
||||||
'that are usable a specific number of times, so customers can see how many times they have '
|
'that are usable a specific number of times, so customers can see how many times they have '
|
||||||
'already been used. Exits or failed scans will not be counted, and the user will not see '
|
'already been used. Exits or failed scans will not be counted, and the user will not see '
|
||||||
@@ -2593,6 +2665,15 @@ Your {organizer} team"""))
|
|||||||
label=_("Use round edges"),
|
label=_("Use round edges"),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'widget_use_native_spinners': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool,
|
||||||
|
'form_class': forms.BooleanField,
|
||||||
|
'serializer_class': serializers.BooleanField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Use native spinners in the widget instead of custom ones for numeric inputs such as quantity."),
|
||||||
|
)
|
||||||
|
},
|
||||||
'primary_font': {
|
'primary_font': {
|
||||||
'default': 'Open Sans',
|
'default': 'Open Sans',
|
||||||
'type': str,
|
'type': str,
|
||||||
@@ -3175,7 +3256,7 @@ def concatenation_for_salutation(d):
|
|||||||
|
|
||||||
def get_name_parts_localized(name_parts, key):
|
def get_name_parts_localized(name_parts, key):
|
||||||
value = name_parts.get(key, "")
|
value = name_parts.get(key, "")
|
||||||
if key == "salutation":
|
if key == "salutation" and value:
|
||||||
return pgettext_lazy("person_name_salutation", value)
|
return pgettext_lazy("person_name_salutation", value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -3320,6 +3401,7 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
|||||||
('full_name', _('Full name'), 2),
|
('full_name', _('Full name'), 2),
|
||||||
),
|
),
|
||||||
'concatenation': lambda d: str(d.get('full_name', '')),
|
'concatenation': lambda d: str(d.get('full_name', '')),
|
||||||
|
'concatenation_all_components': lambda d: str(d.get('full_name', '')) + " (\"" + d.get('calling_name', '') + "\")",
|
||||||
'sample': {
|
'sample': {
|
||||||
'full_name': pgettext_lazy('person_name_sample', 'John Doe'),
|
'full_name': pgettext_lazy('person_name_sample', 'John Doe'),
|
||||||
'calling_name': pgettext_lazy('person_name_sample', 'John'),
|
'calling_name': pgettext_lazy('person_name_sample', 'John'),
|
||||||
@@ -3332,6 +3414,7 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
|||||||
('latin_transcription', _('Latin transcription'), 2),
|
('latin_transcription', _('Latin transcription'), 2),
|
||||||
),
|
),
|
||||||
'concatenation': lambda d: str(d.get('full_name', '')),
|
'concatenation': lambda d: str(d.get('full_name', '')),
|
||||||
|
'concatenation_all_components': lambda d: str(d.get('full_name', '')) + " (" + d.get('latin_transcription', '') + ")",
|
||||||
'sample': {
|
'sample': {
|
||||||
'full_name': '庄司',
|
'full_name': '庄司',
|
||||||
'latin_transcription': 'Shōji',
|
'latin_transcription': 'Shōji',
|
||||||
@@ -3348,6 +3431,9 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
|||||||
str(p) for p in (d.get(key, '') for key in ["given_name", "family_name"]) if p
|
str(p) for p in (d.get(key, '') for key in ["given_name", "family_name"]) if p
|
||||||
),
|
),
|
||||||
'concatenation_for_salutation': concatenation_for_salutation,
|
'concatenation_for_salutation': concatenation_for_salutation,
|
||||||
|
'concatenation_all_components': lambda d: ' '.join(
|
||||||
|
str(p) for p in (get_name_parts_localized(d, key) for key in ["salutation", "given_name", "family_name"]) if p
|
||||||
|
),
|
||||||
'sample': {
|
'sample': {
|
||||||
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
|
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
|
||||||
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
||||||
@@ -3366,6 +3452,9 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
|||||||
str(p) for p in (d.get(key, '') for key in ["title", "given_name", "family_name"]) if p
|
str(p) for p in (d.get(key, '') for key in ["title", "given_name", "family_name"]) if p
|
||||||
),
|
),
|
||||||
'concatenation_for_salutation': concatenation_for_salutation,
|
'concatenation_for_salutation': concatenation_for_salutation,
|
||||||
|
'concatenation_all_components': lambda d: ' '.join(
|
||||||
|
str(p) for p in (get_name_parts_localized(d, key) for key in ["salutation", "title", "given_name", "family_name"]) if p
|
||||||
|
),
|
||||||
'sample': {
|
'sample': {
|
||||||
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
|
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
|
||||||
'title': pgettext_lazy('person_name_sample', 'Dr'),
|
'title': pgettext_lazy('person_name_sample', 'Dr'),
|
||||||
@@ -3390,6 +3479,13 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
|||||||
str(d.get('degree', ''))
|
str(d.get('degree', ''))
|
||||||
),
|
),
|
||||||
'concatenation_for_salutation': concatenation_for_salutation,
|
'concatenation_for_salutation': concatenation_for_salutation,
|
||||||
|
'concatenation_all_components': lambda d: (
|
||||||
|
' '.join(
|
||||||
|
str(p) for p in (get_name_parts_localized(d, key) for key in ["salutation", "title", "given_name", "family_name"]) if p
|
||||||
|
) +
|
||||||
|
str((', ' if d.get('degree') else '')) +
|
||||||
|
str(d.get('degree', ''))
|
||||||
|
),
|
||||||
'sample': {
|
'sample': {
|
||||||
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
|
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
|
||||||
'title': pgettext_lazy('person_name_sample', 'Dr'),
|
'title': pgettext_lazy('person_name_sample', 'Dr'),
|
||||||
|
|||||||
@@ -304,8 +304,7 @@ multiple events. Receivers should return a subclass of pretix.base.exporter.Base
|
|||||||
The ``sender`` keyword argument will contain an organizer.
|
The ``sender`` keyword argument will contain an organizer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
validate_order = EventPluginSignal(
|
validate_order = EventPluginSignal()
|
||||||
)
|
|
||||||
"""
|
"""
|
||||||
Arguments: ``payments``, ``positions``, ``email``, ``locale``, ``invoice_address``,
|
Arguments: ``payments``, ``positions``, ``email``, ``locale``, ``invoice_address``,
|
||||||
``meta_info``, ``customer``
|
``meta_info``, ``customer``
|
||||||
@@ -321,6 +320,18 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
|
|||||||
in the future, as the ``payments`` attribute gives more information.
|
in the future, as the ``payments`` attribute gives more information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
order_valid_if_pending = EventPluginSignal()
|
||||||
|
"""
|
||||||
|
Arguments: ``payments``, ``positions``, ``email``, ``locale``, ``invoice_address``,
|
||||||
|
``meta_info``, ``customer``
|
||||||
|
|
||||||
|
This signal is sent out when the user tries to confirm the order, before we actually create
|
||||||
|
the order. It allows you to set the ``valid_if_pending`` of the order even before it is
|
||||||
|
created. Whenever any plugin returns ``True``, the order will be valid if pending.
|
||||||
|
|
||||||
|
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
|
"""
|
||||||
|
|
||||||
validate_cart = EventPluginSignal()
|
validate_cart = EventPluginSignal()
|
||||||
"""
|
"""
|
||||||
Arguments: ``positions``
|
Arguments: ``positions``
|
||||||
@@ -578,6 +589,20 @@ All plugins that are installed may send fields for the global settings form, as
|
|||||||
an OrderedDict of (setting name, form field).
|
an OrderedDict of (setting name, form field).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
gift_card_transaction_display = django.dispatch.Signal()
|
||||||
|
"""
|
||||||
|
Arguments: ``transaction``, ``customer_facing``
|
||||||
|
|
||||||
|
To display an instance of the ``GiftCardTransaction`` model to a human user,
|
||||||
|
``pretix.base.signals.gift_card_transaction_display`` will be sent out with a ``transaction`` argument.
|
||||||
|
The ``customer_facing`` argument specifies whether the HTML will be shown to an end-user or if it is being
|
||||||
|
used in the backend.
|
||||||
|
|
||||||
|
The first received response that is not ``None`` will be used to display the log entry
|
||||||
|
to the user. The receivers are expected to return a string (that might be marked with ``mark_safe`` from Django if
|
||||||
|
it contains HTML).
|
||||||
|
"""
|
||||||
|
|
||||||
order_fee_calculation = EventPluginSignal()
|
order_fee_calculation = EventPluginSignal()
|
||||||
"""
|
"""
|
||||||
Arguments: ``positions``, ``invoice_address``, ``meta_info``, ``total``, ``gift_cards``, ``payment_requests``
|
Arguments: ``positions``, ``invoice_address``, ``meta_info``, ``total``, ``gift_cards``, ``payment_requests``
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}" />
|
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}" />
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixbase/js/reloadpending.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixbase/js/reloadpending.js" %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import re
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
import idna
|
|
||||||
import markdown
|
import markdown
|
||||||
from bleach import DEFAULT_CALLBACKS
|
from bleach import DEFAULT_CALLBACKS
|
||||||
from bleach.linkifier import build_email_re, build_url_re
|
from bleach.linkifier import build_email_re, build_url_re
|
||||||
@@ -122,12 +121,6 @@ def safelink_callback(attrs, new=False):
|
|||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
def idna_decode_safe(src):
|
|
||||||
v = idna.decode(src)
|
|
||||||
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
def truelink_callback(attrs, new=False):
|
def truelink_callback(attrs, new=False):
|
||||||
"""
|
"""
|
||||||
Tries to prevent "phishing" attacks in which a link looks like it points to a safe place but instead
|
Tries to prevent "phishing" attacks in which a link looks like it points to a safe place but instead
|
||||||
@@ -143,40 +136,20 @@ def truelink_callback(attrs, new=False):
|
|||||||
|
|
||||||
<a href="https://maps.google.com/location/foo">https://maps.google.com</a>
|
<a href="https://maps.google.com/location/foo">https://maps.google.com</a>
|
||||||
"""
|
"""
|
||||||
text = re.sub(r'[^a-zA-Z0-9:.\-/_ ]', '', attrs.get('_text')) # clean up link text
|
text = re.sub(r'[^a-zA-Z0-9.\-/_ ]', '', attrs.get('_text')) # clean up link text
|
||||||
url = attrs.get((None, 'href'), '/')
|
url = attrs.get((None, 'href'), '/')
|
||||||
href_url = urllib.parse.urlparse(url)
|
href_url = urllib.parse.urlparse(url)
|
||||||
strip_http = False
|
|
||||||
if (None, 'href') in attrs and URL_RE.match(text) and href_url.scheme not in ('tel', 'mailto'):
|
if (None, 'href') in attrs and URL_RE.match(text) and href_url.scheme not in ('tel', 'mailto'):
|
||||||
if URL_RE.match(attrs.get('_text').strip()): # maybe we cleaned up too much
|
|
||||||
text = attrs.get('_text').strip()
|
|
||||||
# link text looks like a url
|
# link text looks like a url
|
||||||
if text.startswith('//'):
|
if text.startswith('//'):
|
||||||
text = 'https:' + text
|
text = 'https:' + text
|
||||||
elif not text.startswith('http'):
|
elif not text.startswith('http'):
|
||||||
strip_http = True
|
|
||||||
text = 'https://' + text
|
text = 'https://' + text
|
||||||
|
|
||||||
text_url = urllib.parse.urlparse(text)
|
text_url = urllib.parse.urlparse(text)
|
||||||
|
if text_url.netloc != href_url.netloc or not href_url.path.startswith(href_url.path):
|
||||||
if href_url.netloc.startswith('xn--'):
|
|
||||||
href_netloc_nice = idna_decode_safe(href_url.netloc)
|
|
||||||
else:
|
|
||||||
href_netloc_nice = href_url.netloc
|
|
||||||
|
|
||||||
if text_url.netloc not in (href_url.netloc, href_netloc_nice) or not href_url.path.startswith(text_url.path):
|
|
||||||
# link text contains an URL that has a different base than the actual URL
|
# link text contains an URL that has a different base than the actual URL
|
||||||
attrs['_text'] = attrs[None, 'href']
|
attrs['_text'] = attrs[None, 'href']
|
||||||
text_url = href_url
|
|
||||||
|
|
||||||
if text_url.netloc.startswith('xn--'):
|
|
||||||
# Show punicode nicely
|
|
||||||
text_netloc_nice = idna_decode_safe(text_url.netloc)
|
|
||||||
url = text_url._replace(netloc=text_netloc_nice, scheme=href_url.scheme).geturl()
|
|
||||||
if strip_http:
|
|
||||||
url = url[len(href_url.scheme + '://'):]
|
|
||||||
attrs['_text'] = url
|
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -542,6 +542,7 @@ class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
|||||||
'waiting_list_phones_asked',
|
'waiting_list_phones_asked',
|
||||||
'waiting_list_phones_required',
|
'waiting_list_phones_required',
|
||||||
'waiting_list_phones_explanation_text',
|
'waiting_list_phones_explanation_text',
|
||||||
|
'waiting_list_limit_per_user',
|
||||||
'max_items_per_order',
|
'max_items_per_order',
|
||||||
'reservation_time',
|
'reservation_time',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
@@ -855,6 +856,8 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
|||||||
'invoice_footer_text',
|
'invoice_footer_text',
|
||||||
'invoice_eu_currencies',
|
'invoice_eu_currencies',
|
||||||
'invoice_logo_image',
|
'invoice_logo_image',
|
||||||
|
'invoice_renderer_highlight_order_code',
|
||||||
|
'invoice_renderer_font',
|
||||||
]
|
]
|
||||||
|
|
||||||
invoice_generate_sales_channels = forms.MultipleChoiceField(
|
invoice_generate_sales_channels = forms.MultipleChoiceField(
|
||||||
@@ -1470,6 +1473,7 @@ class TaxRuleForm(I18nModelForm):
|
|||||||
class WidgetCodeForm(forms.Form):
|
class WidgetCodeForm(forms.Form):
|
||||||
subevent = forms.ModelChoiceField(
|
subevent = forms.ModelChoiceField(
|
||||||
label=pgettext_lazy('subevent', "Date"),
|
label=pgettext_lazy('subevent', "Date"),
|
||||||
|
empty_label=pgettext_lazy('subevent', "All dates"),
|
||||||
required=False,
|
required=False,
|
||||||
queryset=SubEvent.objects.none()
|
queryset=SubEvent.objects.none()
|
||||||
)
|
)
|
||||||
@@ -1670,7 +1674,7 @@ QuickSetupProductFormSet = formset_factory(
|
|||||||
|
|
||||||
class ItemMetaPropertyForm(forms.ModelForm):
|
class ItemMetaPropertyForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ['name', 'default']
|
fields = ['name', 'default', 'required', 'allowed_values']
|
||||||
widgets = {
|
widgets = {
|
||||||
'default': forms.TextInput()
|
'default': forms.TextInput()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ class EventOrderFilterForm(OrderFilterForm):
|
|||||||
file__isnull=False
|
file__isnull=False
|
||||||
)
|
)
|
||||||
qs = qs.annotate(has_answer=Exists(answers)).filter(has_answer=True)
|
qs = qs.annotate(has_answer=Exists(answers)).filter(has_answer=True)
|
||||||
elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE) and fdata.get('answer'):
|
||||||
answers = QuestionAnswer.objects.filter(
|
answers = QuestionAnswer.objects.filter(
|
||||||
question_id=q.pk,
|
question_id=q.pk,
|
||||||
orderposition__order_id=OuterRef('pk'),
|
orderposition__order_id=OuterRef('pk'),
|
||||||
@@ -1338,6 +1338,7 @@ class GiftCardFilterForm(FilterForm):
|
|||||||
Q(secret__icontains=query)
|
Q(secret__icontains=query)
|
||||||
| Q(transactions__text__icontains=query)
|
| Q(transactions__text__icontains=query)
|
||||||
| Q(transactions__order__code__icontains=query)
|
| Q(transactions__order__code__icontains=query)
|
||||||
|
| Q(owner_ticket__order__code__icontains=query)
|
||||||
)
|
)
|
||||||
if fdata.get('testmode') == 'yes':
|
if fdata.get('testmode') == 'yes':
|
||||||
qs = qs.filter(testmode=True)
|
qs = qs.filter(testmode=True)
|
||||||
|
|||||||
@@ -1102,16 +1102,26 @@ class ItemMetaValueForm(forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.property = kwargs.pop('property')
|
self.property = kwargs.pop('property')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['value'].required = False
|
if self.property.allowed_values:
|
||||||
self.fields['value'].widget.attrs['placeholder'] = self.property.default
|
self.fields['value'] = forms.ChoiceField(
|
||||||
self.fields['value'].widget.attrs['data-typeahead-url'] = (
|
label=self.property.name,
|
||||||
reverse('control:event.items.meta.typeahead', kwargs={
|
choices=[(
|
||||||
'organizer': self.property.event.organizer.slug,
|
"", _("Default ({value})").format(value=self.property.default)
|
||||||
'event': self.property.event.slug
|
if self.property.default else ""
|
||||||
}) + '?' + urlencode({
|
)] + [(a.strip(), a.strip()) for a in self.property.allowed_values.splitlines()]
|
||||||
'property': self.property.name,
|
)
|
||||||
})
|
else:
|
||||||
)
|
self.fields['value'].label = self.property.name
|
||||||
|
self.fields['value'].widget.attrs['placeholder'] = self.property.default
|
||||||
|
self.fields['value'].widget.attrs['data-typeahead-url'] = (
|
||||||
|
reverse('control:event.items.meta.typeahead', kwargs={
|
||||||
|
'organizer': self.property.event.organizer.slug,
|
||||||
|
'event': self.property.event.slug
|
||||||
|
}) + '?' + urlencode({
|
||||||
|
'property': self.property.name,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
self.fields['value'].required = self.property.required and not self.property.default
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemMetaValue
|
model = ItemMetaValue
|
||||||
|
|||||||
@@ -186,6 +186,15 @@ class OrganizerUpdateForm(OrganizerForm):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class SafeOrderPositionChoiceField(forms.ModelChoiceField):
|
||||||
|
def __init__(self, queryset, **kwargs):
|
||||||
|
queryset = queryset.model.all.none()
|
||||||
|
super().__init__(queryset, **kwargs)
|
||||||
|
|
||||||
|
def label_from_instance(self, op):
|
||||||
|
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
|
||||||
|
|
||||||
|
|
||||||
class EventMetaPropertyForm(forms.ModelForm):
|
class EventMetaPropertyForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = EventMetaProperty
|
model = EventMetaProperty
|
||||||
@@ -264,7 +273,7 @@ class DeviceForm(forms.ModelForm):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
d = super().clean()
|
d = super().clean()
|
||||||
if not d['all_events'] and not d['limit_events']:
|
if not d['all_events'] and not d.get('limit_events'):
|
||||||
raise ValidationError(_('Your device will not have access to anything, please select some events.'))
|
raise ValidationError(_('Your device will not have access to anything, please select some events.'))
|
||||||
|
|
||||||
return d
|
return d
|
||||||
@@ -650,23 +659,32 @@ class GiftCardCreateForm(forms.ModelForm):
|
|||||||
class GiftCardUpdateForm(forms.ModelForm):
|
class GiftCardUpdateForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GiftCard
|
model = GiftCard
|
||||||
fields = ['expires', 'conditions']
|
fields = ['expires', 'conditions', 'owner_ticket']
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'expires': SplitDateTimeField
|
'expires': SplitDateTimeField,
|
||||||
|
'owner_ticket': SafeOrderPositionChoiceField,
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'expires': SplitDateTimePickerWidget,
|
'expires': SplitDateTimePickerWidget,
|
||||||
'conditions': forms.Textarea(attrs={"rows": 2})
|
'conditions': forms.Textarea(attrs={"rows": 2})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
organizer = self.instance.issuer
|
||||||
|
|
||||||
class SafeOrderPositionChoiceField(forms.ModelChoiceField):
|
self.fields['owner_ticket'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
|
||||||
def __init__(self, queryset, **kwargs):
|
self.fields['owner_ticket'].widget = Select2(
|
||||||
queryset = queryset.model.all.none()
|
attrs={
|
||||||
super().__init__(queryset, **kwargs)
|
'data-model-select2': 'generic',
|
||||||
|
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
|
||||||
def label_from_instance(self, op):
|
'organizer': organizer.slug,
|
||||||
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
|
}),
|
||||||
|
'data-placeholder': _('Ticket')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.fields['owner_ticket'].widget.choices = self.fields['owner_ticket'].choices
|
||||||
|
self.fields['owner_ticket'].required = False
|
||||||
|
|
||||||
|
|
||||||
class ReusableMediumUpdateForm(forms.ModelForm):
|
class ReusableMediumUpdateForm(forms.ModelForm):
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ class VoucherForm(I18nModelForm):
|
|||||||
localized_fields = '__all__'
|
localized_fields = '__all__'
|
||||||
fields = [
|
fields = [
|
||||||
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
|
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
|
||||||
'comment', 'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
|
'comment', 'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included',
|
||||||
|
'all_bundles_included', 'budget'
|
||||||
]
|
]
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'valid_until': SplitDateTimeField,
|
'valid_until': SplitDateTimeField,
|
||||||
@@ -308,7 +309,8 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
localized_fields = '__all__'
|
localized_fields = '__all__'
|
||||||
fields = [
|
fields = [
|
||||||
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
|
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
|
||||||
'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
|
'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included',
|
||||||
|
'all_bundles_included', 'budget'
|
||||||
]
|
]
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'valid_until': SplitDateTimeField,
|
'valid_until': SplitDateTimeField,
|
||||||
@@ -384,17 +386,18 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
data = super().clean()
|
data = super().clean()
|
||||||
|
|
||||||
vouchers = self.instance.event.vouchers.annotate(
|
if 'codes' in data:
|
||||||
code_upper=Upper('code')
|
vouchers = self.instance.event.vouchers.annotate(
|
||||||
).filter(code_upper__in=[c.upper() for c in data['codes']])
|
code_upper=Upper('code')
|
||||||
if vouchers.exists():
|
).filter(code_upper__in=[c.upper() for c in data['codes']])
|
||||||
raise ValidationError(_('A voucher with one of these codes already exists.'))
|
if vouchers.exists():
|
||||||
|
raise ValidationError(_('A voucher with one of these codes already exists.'))
|
||||||
|
|
||||||
codes_seen = set()
|
codes_seen = set()
|
||||||
for c in data['codes']:
|
for c in data['codes']:
|
||||||
if c in codes_seen:
|
if c in codes_seen:
|
||||||
raise ValidationError(_('The voucher code {code} appears in your list twice.').format(code=c))
|
raise ValidationError(_('The voucher code {code} appears in your list twice.').format(code=c))
|
||||||
codes_seen.add(c)
|
codes_seen.add(c)
|
||||||
|
|
||||||
if data.get('send') and not all([data.get('send_subject'), data.get('send_message'), data.get('send_recipients')]):
|
if data.get('send') and not all([data.get('send_subject'), data.get('send_message'), data.get('send_recipients')]):
|
||||||
raise ValidationError(_('If vouchers should be sent by email, subject, message and recipients need to be specified.'))
|
raise ValidationError(_('If vouchers should be sent by email, subject, message and recipients need to be specified.'))
|
||||||
|
|||||||
@@ -485,6 +485,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
'pretix.event.item.bundles.added': _('A bundled item has been added to this product.'),
|
'pretix.event.item.bundles.added': _('A bundled item has been added to this product.'),
|
||||||
'pretix.event.item.bundles.removed': _('A bundled item has been removed from this product.'),
|
'pretix.event.item.bundles.removed': _('A bundled item has been removed from this product.'),
|
||||||
'pretix.event.item.bundles.changed': _('A bundled item has been changed on this product.'),
|
'pretix.event.item.bundles.changed': _('A bundled item has been changed on this product.'),
|
||||||
|
'pretix.event.item_meta_property.added': _('A meta property has been added to this event.'),
|
||||||
|
'pretix.event.item_meta_property.deleted': _('A meta property has been removed from this event.'),
|
||||||
|
'pretix.event.item_meta_property.changed': _('A meta property has been changed on this event.'),
|
||||||
'pretix.event.quota.added': _('The quota has been added.'),
|
'pretix.event.quota.added': _('The quota has been added.'),
|
||||||
'pretix.event.quota.deleted': _('The quota has been deleted.'),
|
'pretix.event.quota.deleted': _('The quota has been deleted.'),
|
||||||
'pretix.event.quota.changed': _('The quota has been changed.'),
|
'pretix.event.quota.changed': _('The quota has been changed.'),
|
||||||
@@ -506,6 +509,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),
|
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),
|
||||||
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
|
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
|
||||||
'pretix.event.checkinlist.deleted': _('The check-in list has been deleted.'),
|
'pretix.event.checkinlist.deleted': _('The check-in list has been deleted.'),
|
||||||
|
'pretix.event.checkinlists.deleted': _('The check-in list has been deleted.'), # backwards compatibility
|
||||||
'pretix.event.checkinlist.changed': _('The check-in list has been changed.'),
|
'pretix.event.checkinlist.changed': _('The check-in list has been changed.'),
|
||||||
'pretix.event.settings': _('The event settings have been changed.'),
|
'pretix.event.settings': _('The event settings have been changed.'),
|
||||||
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
|
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
|
||||||
@@ -567,6 +571,17 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
else:
|
else:
|
||||||
data['value'] = LazyI18nString(data['value'])
|
data['value'] = LazyI18nString(data['value'])
|
||||||
|
|
||||||
|
if logentry.action_type == "pretix.voucher.redeemed":
|
||||||
|
data = defaultdict(lambda: '?', data)
|
||||||
|
url = reverse('control:event.order', kwargs={
|
||||||
|
'event': logentry.event.slug,
|
||||||
|
'organizer': logentry.event.organizer.slug,
|
||||||
|
'code': data['order_code']
|
||||||
|
})
|
||||||
|
return mark_safe(plains[logentry.action_type].format(
|
||||||
|
order_code='<a href="{}">{}</a>'.format(url, data['order_code']),
|
||||||
|
))
|
||||||
|
|
||||||
if logentry.action_type in plains:
|
if logentry.action_type in plains:
|
||||||
data = defaultdict(lambda: '?', data)
|
data = defaultdict(lambda: '?', data)
|
||||||
return plains[logentry.action_type].format_map(data)
|
return plains[logentry.action_type].format_map(data)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<script src="{% statici18n request.LANGUAGE_CODE %}"></script>
|
<script src="{% statici18n request.LANGUAGE_CODE %}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "js/jquery.formset.js" %}"></script>
|
<script type="text/javascript" src="{% static "js/jquery.formset.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "typeahead/typeahead.bundle.js" %}"></script>
|
<script type="text/javascript" src="{% static "typeahead/typeahead.bundle.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "bootstrap/js/bootstrap.js" %}"></script>
|
<script type="text/javascript" src="{% static "bootstrap/js/bootstrap.js" %}"></script>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<div class="helper-space-below">
|
<div class="helper-space-below">
|
||||||
{% trans "Shop URL:" %}
|
{% trans "Shop URL:" %}
|
||||||
<span id="shop_url" class="text-muted">{% humanabseventurl request.event "presale:event.index" %}</span>
|
<span id="shop_url" class="text-muted">{% abseventurl request.event "presale:event.index" %}</span>
|
||||||
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#shop_url">
|
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#shop_url">
|
||||||
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
||||||
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
|
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
|
||||||
|
|||||||
@@ -54,6 +54,8 @@
|
|||||||
{% bootstrap_field form.invoice_additional_text layout="control" %}
|
{% bootstrap_field form.invoice_additional_text layout="control" %}
|
||||||
{% bootstrap_field form.invoice_footer_text layout="control" %}
|
{% bootstrap_field form.invoice_footer_text layout="control" %}
|
||||||
{% bootstrap_field form.invoice_logo_image layout="control" %}
|
{% bootstrap_field form.invoice_logo_image layout="control" %}
|
||||||
|
{% bootstrap_field form.invoice_renderer_font layout="control" %}
|
||||||
|
{% bootstrap_field form.invoice_renderer_highlight_order_code layout="control" %}
|
||||||
{% bootstrap_field form.invoice_eu_currencies layout="control" %}
|
{% bootstrap_field form.invoice_eu_currencies layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -361,6 +361,7 @@
|
|||||||
{% bootstrap_field sform.waiting_list_names_asked_required layout="control" %}
|
{% bootstrap_field sform.waiting_list_names_asked_required layout="control" %}
|
||||||
{% bootstrap_field sform.waiting_list_phones_asked_required layout="control" %}
|
{% bootstrap_field sform.waiting_list_phones_asked_required layout="control" %}
|
||||||
{% bootstrap_field sform.waiting_list_phones_explanation_text layout="control" %}
|
{% bootstrap_field sform.waiting_list_phones_explanation_text layout="control" %}
|
||||||
|
{% bootstrap_field sform.waiting_list_limit_per_user layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Item metadata" %}</legend>
|
<legend>{% trans "Item metadata" %}</legend>
|
||||||
@@ -377,41 +378,55 @@
|
|||||||
{% bootstrap_formset_errors item_meta_property_formset %}
|
{% bootstrap_formset_errors item_meta_property_formset %}
|
||||||
<div data-formset-body>
|
<div data-formset-body>
|
||||||
{% for form in item_meta_property_formset %}
|
{% for form in item_meta_property_formset %}
|
||||||
<div class="row formset-row" data-formset-form>
|
<div class="panel panel-default" data-formset-form>
|
||||||
<div class="sr-only">
|
<div class="sr-only">
|
||||||
{{ form.id }}
|
{{ form.id }}
|
||||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5">
|
<div class="panel-heading">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<h3 class="panel-title">{% trans "Property" %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4 text-right flip">
|
||||||
|
<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_form_errors form %}
|
||||||
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
{% bootstrap_field form.name layout="control" %}
|
||||||
</div>
|
{% bootstrap_field form.default layout="control" %}
|
||||||
<div class="col-md-5 col-lg-6">
|
{% bootstrap_field form.required layout="control" %}
|
||||||
{% bootstrap_field form.default layout='inline' form_group_class="" %}
|
{% bootstrap_field form.allowed_values layout="control" %}
|
||||||
</div>
|
|
||||||
<div class="col-md-2 col-lg-1 text-right flip">
|
|
||||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
|
||||||
<i class="fa fa-trash"></i></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<script type="form-template" data-formset-empty-form>
|
<script type="form-template" data-formset-empty-form>
|
||||||
{% escapescript %}
|
{% escapescript %}
|
||||||
<div class="row formset-row" data-formset-form>
|
<div class="panel panel-default" data-formset-form>
|
||||||
<div class="sr-only">
|
<div class="sr-only">
|
||||||
{{ item_meta_property_formset.empty_form.id }}
|
{{ item_meta_property_formset.empty_form.id }}
|
||||||
{% bootstrap_field item_meta_property_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
{% bootstrap_field item_meta_property_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5">
|
<div class="panel-heading">
|
||||||
{% bootstrap_field item_meta_property_formset.empty_form.name layout='inline' form_group_class="" %}
|
<div class="row">
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<h3 class="panel-title">{% trans "Property" %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4 text-right flip">
|
||||||
|
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
|
||||||
|
<i class="fa fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5 col-lg-6">
|
<div class="panel-body form-horizontal">
|
||||||
{% bootstrap_field item_meta_property_formset.empty_form.default layout='inline' form_group_class="" %}
|
{% bootstrap_field item_meta_property_formset.empty_form.name layout="control" %}
|
||||||
</div>
|
{% bootstrap_field item_meta_property_formset.empty_form.default layout="control" %}
|
||||||
<div class="col-md-2 col-lg-1 text-right flip">
|
{% bootstrap_field item_meta_property_formset.empty_form.required layout="control" %}
|
||||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
{% bootstrap_field item_meta_property_formset.empty_form.allowed_values layout="control" %}
|
||||||
<i class="fa fa-trash"></i></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endescapescript %}
|
{% endescapescript %}
|
||||||
|
|||||||
@@ -139,6 +139,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if meta_forms %}
|
||||||
|
<div class="form-group metadata-group">
|
||||||
|
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% for form in meta_forms %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="{{ form.value.id_for_label }}">
|
||||||
|
{{ form.property.name }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
{% bootstrap_form form layout="inline" error_types="all" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% if form.quota_option %}
|
{% if form.quota_option %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|||||||
@@ -134,7 +134,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
{% bootstrap_form form layout="inline" %}
|
{% bootstrap_form form layout="inline" error_types="all" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<noscript>
|
<noscript>
|
||||||
<p>{% trans "Only applicable if you choose 'Choose one/multiple from a list' above." %}</p>
|
<p>{% trans "Only applicable if you choose 'Choose one/multiple from a list' above." %}</p>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}" data-formset-delete-confirm-text="{% trans "If you delete an answer option, you will no longer be able to see statistical data on customers who previously selected this option, and when such customers edit their answers, they need to select a different option." %}">
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
{% bootstrap_formset_errors formset %}
|
{% bootstrap_formset_errors formset %}
|
||||||
<div data-formset-body>
|
<div data-formset-body>
|
||||||
|
|||||||
@@ -511,7 +511,7 @@
|
|||||||
<dl>
|
<dl>
|
||||||
{% if line.item.ask_attendee_data and event.settings.attendee_names_asked %}
|
{% if line.item.ask_attendee_data and event.settings.attendee_names_asked %}
|
||||||
<dt>{% trans "Attendee name" %}</dt>
|
<dt>{% trans "Attendee name" %}</dt>
|
||||||
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}
|
<dd>{% if line.attendee_name %}{{ line.attendee_name_all_components }}{% else %}
|
||||||
<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if line.item.ask_attendee_data and event.settings.attendee_emails_asked %}
|
{% if line.item.ask_attendee_data and event.settings.attendee_emails_asked %}
|
||||||
@@ -631,6 +631,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
|
{% for gc in line.owned_gift_cards.all %}
|
||||||
|
<div class="product-row-giftcard">
|
||||||
|
<span class="fa fa-credit-card" aria-hidden="true"></span>
|
||||||
|
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">{{ gc.secret }}</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for fee in items.fees %}
|
{% for fee in items.fees %}
|
||||||
<div class="row-fluid product-row {% if fee.canceled %}pos-canceled{% endif %}">
|
<div class="row-fluid product-row {% if fee.canceled %}pos-canceled{% endif %}">
|
||||||
|
|||||||
@@ -204,13 +204,21 @@
|
|||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</th>
|
</th>
|
||||||
<th class="text-right flip">
|
<th class="text-right flip">
|
||||||
{% if sums.s|default_if_none:"none" != "none" %}
|
{% if not filter_form.filtered %}
|
||||||
{{ sums.s|money:request.event.currency }}
|
<span class="fa fa-info-circle text-muted" data-toggle="tooltip"
|
||||||
|
title='{% trans 'This sum includes canceled orders. For your ticket revenue, look at the "order overview".' %}'></span>
|
||||||
|
{% if sums.s|default_if_none:"none" != "none" %}
|
||||||
|
{{ sums.s|money:request.event.currency }}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
<th class="text-right flip">
|
<th class="text-right flip">
|
||||||
{% if sums.pc %}
|
{% if not filter_form.filtered %}
|
||||||
{{ sums.pc }}
|
<span class="fa fa-info-circle text-muted" data-toggle="tooltip"
|
||||||
|
title='{% trans 'This sum includes canceled orders. For your ticket revenue, look at the "order overview".' %}'></span>
|
||||||
|
{% if sums.pc %}
|
||||||
|
{{ sums.pc }}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
|||||||
@@ -45,6 +45,14 @@
|
|||||||
{% bootstrap_field filter_form.date_until layout='inline' %}
|
{% bootstrap_field filter_form.date_until layout='inline' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-danger">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Filtering this report by date is not recommended as it might lead to misleading information since this
|
||||||
|
report only sees the current state of any order, not any changes made to the order previously.
|
||||||
|
This date filter might be removed in the future.
|
||||||
|
Use the "Accounting report" in the export section instead.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<button class="btn btn-primary btn-lg" type="submit">
|
<button class="btn btn-primary btn-lg" type="submit">
|
||||||
<span class="fa fa-filter"></span>
|
<span class="fa fa-filter"></span>
|
||||||
|
|||||||
@@ -162,16 +162,19 @@
|
|||||||
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
|
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
|
||||||
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
|
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
|
||||||
{% trans "Connect" %}</a>
|
{% trans "Connect" %}</a>
|
||||||
{% elif d.api_token %}
|
{% endif %}
|
||||||
|
{% if not d.initialized or d.api_token %}
|
||||||
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
|
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
|
||||||
class="btn btn-default btn-sm">
|
class="btn btn-default btn-sm">
|
||||||
{% trans "Revoke access" %}</a>
|
{% trans "Revoke access" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url "control:organizer.device.logs" organizer=request.organizer.slug device=d.id %}"
|
{% if d.initialized %}
|
||||||
class="btn btn-default btn-sm">
|
<a href="{% url "control:organizer.device.logs" organizer=request.organizer.slug device=d.id %}"
|
||||||
<span class="fa fa-list-alt"></span>
|
class="btn btn-default btn-sm">
|
||||||
{% trans "Logs" %}
|
<span class="fa fa-list-alt"></span>
|
||||||
</a>
|
{% trans "Logs" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
|
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
|
||||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -97,7 +97,13 @@
|
|||||||
<div class="list-group large-link-group">
|
<div class="list-group large-link-group">
|
||||||
{% for e in c_ex %}
|
{% for e in c_ex %}
|
||||||
<a class="list-group-item" href="?identifier={{ e.identifier }}">
|
<a class="list-group-item" href="?identifier={{ e.identifier }}">
|
||||||
<h4>{{ e.verbose_name }}</h4>
|
<h4>
|
||||||
|
{{ e.verbose_name }}
|
||||||
|
{% if e.featured %}
|
||||||
|
<span class="fa fa-star text-success" data-toggle="tooltip"
|
||||||
|
title="{% trans "Recommended for new users" %}" aria-hidden="true"></span>
|
||||||
|
{% endif %}
|
||||||
|
</h4>
|
||||||
{% if e.description %}
|
{% if e.description %}
|
||||||
<p>
|
<p>
|
||||||
{{ e.description }}
|
{{ e.description }}
|
||||||
|
|||||||
@@ -46,6 +46,14 @@
|
|||||||
{{ card.issued_in.order.full_code }}</a>-{{ card.issued_in.positionid }}
|
{{ card.issued_in.order.full_code }}</a>-{{ card.issued_in.positionid }}
|
||||||
</dd>
|
</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if card.owner_ticket %}
|
||||||
|
<dt>{% trans "Owned by ticket holder" %}</dt>
|
||||||
|
<dd>
|
||||||
|
<span class="fa fa-ticket fa-fw"></span>
|
||||||
|
<a href="{% url "control:event.order" event=card.owner_ticket.order.event.slug organizer=request.organizer.slug code=card.owner_ticket.order.code %}">
|
||||||
|
{{ card.owner_ticket.order.code }}</a>-{{ card.owner_ticket.positionid }}
|
||||||
|
</dd>
|
||||||
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,29 +70,32 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Date" %}</th>
|
<th>{% trans "Date" %}</th>
|
||||||
<th>{% trans "Order" %}</th>
|
<th>{% trans "Information" %}</th>
|
||||||
<th class="text-right">{% trans "Value" %}</th>
|
<th class="text-right">{% trans "Value" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for t in card.transactions.all %}
|
{% for t in transactions %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ t.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
|
<td>{{ t.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if t.order %}
|
{{ t.display_backend }}
|
||||||
<a href="{% url "control:event.order" event=t.order.event.slug organizer=t.order.event.organizer.slug code=t.order.code %}">
|
{% if t.refund and t.value > 0 and t.value <= card.value %}
|
||||||
{{ t.order.full_code }}
|
<button type="submit" name="revert" value="{{ t.pk }}"
|
||||||
</a>
|
class="btn btn-default btn-xs" data-toggle="tooltip"
|
||||||
{% if t.refund and t.value > 0 and t.value <= card.value %}
|
title="{% trans "Create a payment on the respective order that cancels out with this transaction. The order will then likely be overpaid." %}">
|
||||||
<button type="submit" name="revert" value="{{ t.pk }}"
|
<span class="fa fa-repeat"></span>
|
||||||
class="btn btn-default btn-xs" data-toggle="tooltip"
|
{% trans "Revert" %}
|
||||||
title="{% trans "Create a payment on the respective order that cancels out with this transaction. The order will then likely be overpaid." %}">
|
</button>
|
||||||
<span class="fa fa-repeat"></span>
|
{% endif %}
|
||||||
{% trans "Revert" %}
|
{% if staff_session and t.info %}
|
||||||
</button>
|
<pre><code>{{ t.info|pprint }}</code></pre>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% if t.acceptor and t.acceptor != request.organizer %}
|
||||||
<em>{% trans "Manual transaction" %}{% if t.text %}: {{ t.text }}{% endif %}</em>
|
<span class="text-muted">
|
||||||
|
<br>
|
||||||
|
<span class="fa fa-group"></span> {{ t.acceptor }}
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% bootstrap_form_errors form %}
|
{% bootstrap_form_errors form %}
|
||||||
{% bootstrap_field form.expires layout="control" %}
|
{% bootstrap_field form.expires layout="control" %}
|
||||||
|
{% bootstrap_field form.owner_ticket layout="control" %}
|
||||||
{% bootstrap_field form.conditions layout="control" %}
|
{% bootstrap_field form.conditions layout="control" %}
|
||||||
<div class="form-group submit-group">
|
<div class="form-group submit-group">
|
||||||
<button type="submit" class="btn btn-primary btn-save">
|
<button type="submit" class="btn btn-primary btn-save">
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
|
||||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/focus.js" %}"></script>
|
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/focus.js" %}"></script>
|
||||||
|
|||||||
@@ -78,6 +78,8 @@
|
|||||||
{% bootstrap_field form.tag layout="control" %}
|
{% bootstrap_field form.tag layout="control" %}
|
||||||
{% bootstrap_field form.comment layout="control" %}
|
{% bootstrap_field form.comment layout="control" %}
|
||||||
{% bootstrap_field form.show_hidden_items layout="control" %}
|
{% bootstrap_field form.show_hidden_items layout="control" %}
|
||||||
|
{% bootstrap_field form.all_addons_included layout="control" %}
|
||||||
|
{% bootstrap_field form.all_bundles_included layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Send out emails" %}</legend>
|
<legend>{% trans "Send out emails" %}</legend>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<label class="col-md-3 control-label" for="id_url">{% trans "Voucher link" %}</label>
|
<label class="col-md-3 control-label" for="id_url">{% trans "Voucher link" %}</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<input type="text" name="url"
|
<input type="text" name="url"
|
||||||
value="{% humanabseventurl request.event "presale:event.redeem" %}?voucher={{ voucher.code|urlencode }}{% if voucher.subevent_id %}&subevent={{ voucher.subevent_id }}{% endif %}"
|
value="{% abseventurl request.event "presale:event.redeem" %}?voucher={{ voucher.code|urlencode }}{% if voucher.subevent_id %}&subevent={{ voucher.subevent_id }}{% endif %}"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="id_url" readonly>
|
id="id_url" readonly>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,6 +90,8 @@
|
|||||||
{% bootstrap_field form.tag layout="control" %}
|
{% bootstrap_field form.tag layout="control" %}
|
||||||
{% bootstrap_field form.comment layout="control" %}
|
{% bootstrap_field form.comment layout="control" %}
|
||||||
{% bootstrap_field form.show_hidden_items layout="control" %}
|
{% bootstrap_field form.show_hidden_items layout="control" %}
|
||||||
|
{% bootstrap_field form.all_addons_included layout="control" %}
|
||||||
|
{% bootstrap_field form.all_bundles_included layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -178,7 +178,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% if request.event.settings.waiting_list_names_asked %}
|
{% if request.event.settings.waiting_list_names_asked %}
|
||||||
<td>{{ e.name|default:"" }}</td>
|
<td>{{ e.name_all_components|default:"" }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ e.email }}</td>
|
<td>{{ e.email }}</td>
|
||||||
{% if request.event.settings.waiting_list_phones_asked %}
|
{% if request.event.settings.waiting_list_phones_asked %}
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ class CheckinListDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
|||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
success_url = self.get_success_url()
|
success_url = self.get_success_url()
|
||||||
self.object.checkins.all().delete()
|
self.object.checkins.all().delete()
|
||||||
self.object.log_action(action='pretix.event.checkinlists.deleted', user=request.user)
|
self.object.log_action(action='pretix.event.checkinlist.deleted', user=request.user)
|
||||||
self.object.delete()
|
self.object.delete()
|
||||||
messages.success(self.request, _('The selected list has been deleted.'))
|
messages.success(self.request, _('The selected list has been deleted.'))
|
||||||
return HttpResponseRedirect(success_url)
|
return HttpResponseRedirect(success_url)
|
||||||
|
|||||||
@@ -282,9 +282,19 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
|
|||||||
if form in self.item_meta_property_formset.deleted_forms:
|
if form in self.item_meta_property_formset.deleted_forms:
|
||||||
if not form.instance.pk:
|
if not form.instance.pk:
|
||||||
continue
|
continue
|
||||||
|
form.instance.log_action(
|
||||||
|
'pretix.event.item_meta_property.deleted',
|
||||||
|
user=self.request.user,
|
||||||
|
data=form.cleaned_data
|
||||||
|
)
|
||||||
form.instance.delete()
|
form.instance.delete()
|
||||||
form.instance.pk = None
|
form.instance.pk = None
|
||||||
elif form.has_changed():
|
elif form.has_changed():
|
||||||
|
form.instance.log_action(
|
||||||
|
'pretix.event.item_meta_property.changed',
|
||||||
|
user=self.request.user,
|
||||||
|
data=form.cleaned_data
|
||||||
|
)
|
||||||
form.save()
|
form.save()
|
||||||
|
|
||||||
for form in self.item_meta_property_formset.extra_forms:
|
for form in self.item_meta_property_formset.extra_forms:
|
||||||
@@ -294,6 +304,11 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
|
|||||||
continue
|
continue
|
||||||
form.instance.event = obj
|
form.instance.event = obj
|
||||||
form.save()
|
form.save()
|
||||||
|
form.instance.log_action(
|
||||||
|
'pretix.event.item_meta_property.added',
|
||||||
|
user=self.request.user,
|
||||||
|
data=form.cleaned_data
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def confirm_texts_formset(self):
|
def confirm_texts_formset(self):
|
||||||
@@ -1513,7 +1528,7 @@ class EventQRCode(EventPermissionRequiredMixin, View):
|
|||||||
permission = 'can_change_event_settings'
|
permission = 'can_change_event_settings'
|
||||||
|
|
||||||
def get(self, request, *args, filetype, **kwargs):
|
def get(self, request, *args, filetype, **kwargs):
|
||||||
url = build_absolute_uri(request.event, 'presale:event.index', human_readable=True)
|
url = build_absolute_uri(request.event, 'presale:event.index')
|
||||||
|
|
||||||
qr = qrcode.QRCode(
|
qr = qrcode.QRCode(
|
||||||
version=1,
|
version=1,
|
||||||
|
|||||||
@@ -1213,7 +1213,7 @@ class MetaDataEditorMixin:
|
|||||||
f.instance.delete()
|
f.instance.delete()
|
||||||
|
|
||||||
|
|
||||||
class ItemCreate(EventPermissionRequiredMixin, CreateView):
|
class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
|
||||||
form_class = ItemCreateForm
|
form_class = ItemCreateForm
|
||||||
template_name = 'pretixcontrol/item/create.html'
|
template_name = 'pretixcontrol/item/create.html'
|
||||||
permission = 'can_change_items'
|
permission = 'can_change_items'
|
||||||
@@ -1274,6 +1274,11 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
|
|||||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data()
|
||||||
|
ctx['meta_forms'] = self.meta_forms
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
|
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
|
||||||
form_class = ItemUpdateForm
|
form_class = ItemUpdateForm
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ class OrderDetail(OrderView):
|
|||||||
'item', 'variation', 'addon_to', 'tax_rule', 'used_membership', 'used_membership__membership_type',
|
'item', 'variation', 'addon_to', 'tax_rule', 'used_membership', 'used_membership__membership_type',
|
||||||
'discount',
|
'discount',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'item__questions', 'issued_gift_cards', 'linked_media',
|
'item__questions', 'issued_gift_cards', 'owned_gift_cards', 'linked_media',
|
||||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
|
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
|
||||||
Prefetch('all_checkins', queryset=Checkin.all.select_related('list').order_by('datetime')),
|
Prefetch('all_checkins', queryset=Checkin.all.select_related('list').order_by('datetime')),
|
||||||
).order_by('positionid')
|
).order_by('positionid')
|
||||||
@@ -1512,7 +1512,7 @@ class InvoiceDownload(EventPermissionRequiredMixin, View):
|
|||||||
invoice_pdf_task.apply(args=(self.invoice.pk,))
|
invoice_pdf_task.apply(args=(self.invoice.pk,))
|
||||||
return self.get(request, *args, **kwargs)
|
return self.get(request, *args, **kwargs)
|
||||||
|
|
||||||
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(self.invoice.number)
|
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number))
|
||||||
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
|
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|||||||
@@ -1184,7 +1184,7 @@ class DeviceRevokeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
|
|||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if not self.object.api_token:
|
if self.object.revoked:
|
||||||
messages.success(request, _('This device currently does not have access.'))
|
messages.success(request, _('This device currently does not have access.'))
|
||||||
return redirect(reverse('control:organizer.devices', kwargs={
|
return redirect(reverse('control:organizer.devices', kwargs={
|
||||||
'organizer': self.request.organizer.slug,
|
'organizer': self.request.organizer.slug,
|
||||||
@@ -1452,6 +1452,7 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
|||||||
self.object.transactions.create(
|
self.object.transactions.create(
|
||||||
value=value,
|
value=value,
|
||||||
text=request.POST.get('text') or None,
|
text=request.POST.get('text') or None,
|
||||||
|
acceptor=request.organizer,
|
||||||
)
|
)
|
||||||
self.object.log_action(
|
self.object.log_action(
|
||||||
'pretix.giftcards.transaction.manual',
|
'pretix.giftcards.transaction.manual',
|
||||||
@@ -1471,6 +1472,16 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
|||||||
))
|
))
|
||||||
return self.get(request, *args, **kwargs)
|
return self.get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return super().get_context_data(
|
||||||
|
**kwargs,
|
||||||
|
transactions=self.object.transactions.select_related(
|
||||||
|
'order', 'order__event', 'order__event__organizer', 'payment', 'refund'
|
||||||
|
).prefetch_related(
|
||||||
|
'acceptor'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||||
template_name = 'pretixcontrol/organizers/giftcard_create.html'
|
template_name = 'pretixcontrol/organizers/giftcard_create.html'
|
||||||
@@ -1497,6 +1508,7 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
|||||||
form.instance.issuer = self.request.organizer
|
form.instance.issuer = self.request.organizer
|
||||||
super().form_valid(form)
|
super().form_valid(form)
|
||||||
form.instance.transactions.create(
|
form.instance.transactions.create(
|
||||||
|
acceptor=self.request.organizer,
|
||||||
value=form.cleaned_data['value']
|
value=form.cleaned_data['value']
|
||||||
)
|
)
|
||||||
form.instance.log_action('pretix.giftcards.created', user=self.request.user, data={})
|
form.instance.log_action('pretix.giftcards.created', user=self.request.user, data={})
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ def giftcard_select2(request, **kwargs):
|
|||||||
return JsonResponse(doc)
|
return JsonResponse(doc)
|
||||||
|
|
||||||
|
|
||||||
@organizer_permission_required(("can_manage_reusable_media"))
|
@organizer_permission_required(("can_manage_reusable_media", "can_manage_gift_cards"))
|
||||||
def ticket_select2(request, **kwargs):
|
def ticket_select2(request, **kwargs):
|
||||||
query = request.GET.get('query', '')
|
query = request.GET.get('query', '')
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ from django.conf import settings
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import connection, transaction
|
from django.db import connection, transaction
|
||||||
from django.db.models import Sum
|
from django.db.models import Exists, OuterRef, Sum
|
||||||
from django.http import (
|
from django.http import (
|
||||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
||||||
JsonResponse,
|
JsonResponse,
|
||||||
@@ -56,9 +56,12 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views.generic import (
|
from django.views.generic import (
|
||||||
CreateView, ListView, TemplateView, UpdateView, View,
|
CreateView, ListView, TemplateView, UpdateView, View,
|
||||||
)
|
)
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.email import get_available_placeholders
|
from pretix.base.email import get_available_placeholders
|
||||||
from pretix.base.models import CartPosition, LogEntry, Voucher
|
from pretix.base.models import (
|
||||||
|
CartPosition, LogEntry, Voucher, WaitingListEntry,
|
||||||
|
)
|
||||||
from pretix.base.models.vouchers import generate_codes
|
from pretix.base.models.vouchers import generate_codes
|
||||||
from pretix.base.services.locking import NoLockManager
|
from pretix.base.services.locking import NoLockManager
|
||||||
from pretix.base.services.vouchers import vouchers_send
|
from pretix.base.services.vouchers import vouchers_send
|
||||||
@@ -80,16 +83,17 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
|||||||
template_name = 'pretixcontrol/vouchers/index.html'
|
template_name = 'pretixcontrol/vouchers/index.html'
|
||||||
permission = 'can_view_vouchers'
|
permission = 'can_view_vouchers'
|
||||||
|
|
||||||
|
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = Voucher.annotate_budget_used_orders(self.request.event.vouchers.filter(
|
qs = Voucher.annotate_budget_used_orders(self.request.event.vouchers.exclude(
|
||||||
waitinglistentries__isnull=True
|
Exists(WaitingListEntry.objects.filter(voucher_id=OuterRef('pk')))
|
||||||
).select_related(
|
).select_related(
|
||||||
'item', 'variation', 'seat'
|
'item', 'variation', 'seat'
|
||||||
))
|
))
|
||||||
if self.filter_form.is_valid():
|
if self.filter_form.is_valid():
|
||||||
qs = self.filter_form.filter_qs(qs)
|
qs = self.filter_form.filter_qs(qs)
|
||||||
|
|
||||||
return qs.distinct()
|
return qs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import re
|
||||||
|
|
||||||
|
from text_unidecode import unidecode
|
||||||
|
|
||||||
|
|
||||||
|
def safe_for_filename(val):
|
||||||
|
return re.sub('[^a-zA-Z0-9-_. ]+', '_', unidecode(val))
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
|
|
||||||
|
|
||||||
|
def build_name(parts, concatenation=None, fallback_scheme=None):
|
||||||
|
if not parts:
|
||||||
|
return None
|
||||||
|
if "_legacy" in parts:
|
||||||
|
return parts["_legacy"]
|
||||||
|
if "_scheme" in parts:
|
||||||
|
scheme = PERSON_NAME_SCHEMES[parts["_scheme"]]
|
||||||
|
elif fallback_scheme:
|
||||||
|
scheme = PERSON_NAME_SCHEMES[fallback_scheme() if callable(fallback_scheme) else fallback_scheme]
|
||||||
|
else:
|
||||||
|
raise TypeError("Invalid name given.")
|
||||||
|
if not concatenation or concatenation not in scheme:
|
||||||
|
concatenation = "concatenation"
|
||||||
|
return scheme[concatenation](parts).strip()
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from arabic_reshaper import ArabicReshaper
|
||||||
|
from django.utils.functional import SimpleLazyObject
|
||||||
from PIL.Image import Resampling
|
from PIL.Image import Resampling
|
||||||
from reportlab.lib.utils import ImageReader
|
from reportlab.lib.utils import ImageReader
|
||||||
|
|
||||||
@@ -41,3 +43,9 @@ class ThumbnailingImageReader(ImageReader):
|
|||||||
# file handle if the file is a JPEG, and therefore does not respect the
|
# file handle if the file is a JPEG, and therefore does not respect the
|
||||||
# (smaller) size of the modified image.
|
# (smaller) size of the modified image.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
||||||
|
'delete_harakat': True,
|
||||||
|
'support_ligatures': False,
|
||||||
|
}))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user