forked from CGM_Public/pretix_original
Compare commits
2 Commits
v4.20.4
...
order-filt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73bec00d0c | ||
|
|
879ae386ac |
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -6,7 +6,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
directory: "/src"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
versioning-strategy: increase
|
||||
|
||||
49
.github/workflows/build.yml
vendored
49
.github/workflows/build.yml
vendored
@@ -1,49 +0,0 @@
|
||||
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
|
||||
2
.github/workflows/strings.yml
vendored
2
.github/workflows/strings.yml
vendored
@@ -38,6 +38,7 @@ jobs:
|
||||
run: sudo apt update && sudo apt install gettext
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -e ".[dev]"
|
||||
working-directory: ./src
|
||||
- name: Compile messages
|
||||
run: python manage.py compilemessages
|
||||
working-directory: ./src
|
||||
@@ -63,6 +64,7 @@ jobs:
|
||||
run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -e ".[dev]"
|
||||
working-directory: ./src
|
||||
- name: Spellcheck translations
|
||||
run: potypo
|
||||
working-directory: ./src
|
||||
|
||||
2
.github/workflows/style.yml
vendored
2
.github/workflows/style.yml
vendored
@@ -36,6 +36,7 @@ jobs:
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
|
||||
working-directory: ./src
|
||||
- name: Run isort
|
||||
run: isort -c .
|
||||
working-directory: ./src
|
||||
@@ -56,6 +57,7 @@ jobs:
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -e ".[dev]" mysqlclient psycopg2-binary
|
||||
working-directory: ./src
|
||||
- name: Run flake8
|
||||
run: flake8 .
|
||||
working-directory: ./src
|
||||
|
||||
1
.github/workflows/tests.yml
vendored
1
.github/workflows/tests.yml
vendored
@@ -64,6 +64,7 @@ jobs:
|
||||
run: sudo apt update && sudo apt install gettext mariadb-client
|
||||
- name: Install Python dependencies
|
||||
run: pip3 install --ignore-requires-python -e ".[dev]" mysqlclient psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
|
||||
working-directory: ./src
|
||||
- name: Run checks
|
||||
run: python manage.py check
|
||||
working-directory: ./src
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,4 @@
|
||||
env/
|
||||
build/
|
||||
dist/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.ropeproject
|
||||
|
||||
@@ -5,8 +5,8 @@ tests:
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools
|
||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
||||
- cd src
|
||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
||||
- python manage.py check
|
||||
- make all compress
|
||||
- py.test --reruns 3 -n 3 tests
|
||||
@@ -21,16 +21,15 @@ pypi:
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools check-manifest twine
|
||||
- cd src
|
||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
||||
- python setup.py sdist
|
||||
- pip install dist/pretix-*.tar.gz
|
||||
- python -m pretix migrate
|
||||
- python -m pretix check
|
||||
- cd src
|
||||
- make npminstall
|
||||
- cd ..
|
||||
- check-manifest
|
||||
- python -m build
|
||||
- make npminstall
|
||||
- python setup.py sdist bdist_wheel
|
||||
- twine check dist/*
|
||||
- twine upload dist/*
|
||||
tags:
|
||||
|
||||
26
Dockerfile
26
Dockerfile
@@ -19,8 +19,6 @@ RUN apt-get update && \
|
||||
python3-dev \
|
||||
sudo \
|
||||
supervisor \
|
||||
libmaxminddb0 \
|
||||
libmaxminddb-dev \
|
||||
zlib1g-dev && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
@@ -41,6 +39,18 @@ RUN apt-get update && \
|
||||
ENV LC_ALL=C.UTF-8 \
|
||||
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/supervisord /etc/supervisord
|
||||
COPY deployment/docker/supervisord.all.conf /etc/supervisord.all.conf
|
||||
@@ -48,19 +58,9 @@ COPY deployment/docker/supervisord.web.conf /etc/supervisord.web.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/production_settings.py /pretix/src/production_settings.py
|
||||
COPY pyproject.toml /pretix/pyproject.toml
|
||||
COPY _build /pretix/_build
|
||||
COPY src /pretix/src
|
||||
|
||||
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 cd /pretix/src && python setup.py install
|
||||
|
||||
RUN chmod +x /usr/local/bin/pretix && \
|
||||
rm /etc/nginx/sites-enabled/default && \
|
||||
|
||||
48
MANIFEST.in
48
MANIFEST.in
@@ -1,48 +0,0 @@
|
||||
include LICENSE
|
||||
include README.rst
|
||||
include src/Makefile
|
||||
include _build/backend.py
|
||||
global-include *.proto
|
||||
recursive-include src/pretix/static *
|
||||
recursive-include src/pretix/static.dist *
|
||||
recursive-include src/pretix/locale *
|
||||
recursive-include src/pretix/helpers/locale *
|
||||
recursive-include src/pretix/base/templates *
|
||||
recursive-include src/pretix/control/templates *
|
||||
recursive-include src/pretix/presale/templates *
|
||||
recursive-include src/pretix/plugins/banktransfer/templates *
|
||||
recursive-include src/pretix/plugins/banktransfer/static *
|
||||
recursive-include src/pretix/plugins/manualpayment/templates *
|
||||
recursive-include src/pretix/plugins/manualpayment/static *
|
||||
recursive-include src/pretix/plugins/paypal/templates *
|
||||
recursive-include src/pretix/plugins/paypal/static *
|
||||
recursive-include src/pretix/plugins/paypal2/templates *
|
||||
recursive-include src/pretix/plugins/paypal2/static *
|
||||
recursive-include src/pretix/plugins/src/pretixdroid/templates *
|
||||
recursive-include src/pretix/plugins/src/pretixdroid/static *
|
||||
recursive-include src/pretix/plugins/sendmail/templates *
|
||||
recursive-include src/pretix/plugins/statistics/templates *
|
||||
recursive-include src/pretix/plugins/statistics/static *
|
||||
recursive-include src/pretix/plugins/stripe/templates *
|
||||
recursive-include src/pretix/plugins/stripe/static *
|
||||
recursive-include src/pretix/plugins/ticketoutputpdf/templates *
|
||||
recursive-include src/pretix/plugins/ticketoutputpdf/static *
|
||||
recursive-include src/pretix/plugins/badges/templates *
|
||||
recursive-include src/pretix/plugins/badges/static *
|
||||
recursive-include src/pretix/plugins/returnurl/templates *
|
||||
recursive-include src/pretix/plugins/returnurl/static *
|
||||
recursive-include src/pretix/plugins/webcheckin/templates *
|
||||
recursive-include src/pretix/plugins/webcheckin/static *
|
||||
recursive-include src *.cfg
|
||||
recursive-include src *.csv
|
||||
recursive-include src *.gitkeep
|
||||
recursive-include src *.jpg
|
||||
recursive-include src *.json
|
||||
recursive-include src *.py
|
||||
recursive-include src *.svg
|
||||
recursive-include src *.txt
|
||||
recursive-include src Makefile
|
||||
|
||||
recursive-exclude doc *
|
||||
recursive-exclude deployment *
|
||||
recursive-exclude res *
|
||||
@@ -1,12 +0,0 @@
|
||||
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']
|
||||
]
|
||||
@@ -481,18 +481,3 @@ You can configure the maximum file size for uploading various files::
|
||||
; Max upload size for other files in MiB, defaults to 10 MiB
|
||||
; This includes all file upload type order questions
|
||||
max_size_other = 100
|
||||
|
||||
|
||||
GeoIP
|
||||
-----
|
||||
|
||||
pretix can optionally make use of a GeoIP database for some features. It needs a file in ``mmdb`` format, for example
|
||||
`GeoLite2`_ or `GeoAcumen`_::
|
||||
|
||||
[geoip]
|
||||
path=/var/geoipdata/
|
||||
filename_country=GeoLite2-Country.mmdb
|
||||
|
||||
|
||||
.. _GeoAcumen: https://github.com/geoacumen/geoacumen-country
|
||||
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
|
||||
@@ -16,7 +16,7 @@ Manual installation
|
||||
You can use ``pip`` to update pretix directly to the development branch. Then, upgrade as usual::
|
||||
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix"
|
||||
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix&subdirectory=src"
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updatestyles
|
||||
|
||||
@@ -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 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 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 just listen on the Docker interface by changing the following line in ``/etc/postgresql/<version>/main/postgresql.conf``::
|
||||
|
||||
listen_addresses = 'localhost,172.17.0.1'
|
||||
|
||||
@@ -153,89 +153,4 @@ 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.
|
||||
|
||||
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
|
||||
|
||||
@@ -20,12 +20,6 @@ currency string Currency of the
|
||||
testmode boolean Whether this is a test gift card
|
||||
expires datetime Expiry date (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:
|
||||
@@ -41,17 +35,8 @@ value money (string) Transaction amo
|
||||
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``)
|
||||
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
|
||||
---------
|
||||
|
||||
@@ -87,8 +72,6 @@ Endpoints
|
||||
"testmode": false,
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"owner_ticket": null,
|
||||
"issuer": "bigevents",
|
||||
"value": "13.37"
|
||||
}
|
||||
]
|
||||
@@ -98,10 +81,6 @@ Endpoints
|
||||
: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 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
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
@@ -134,8 +113,6 @@ Endpoints
|
||||
"testmode": false,
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"owner_ticket": null,
|
||||
"issuer": "bigevents",
|
||||
"value": "13.37"
|
||||
}
|
||||
|
||||
@@ -180,16 +157,10 @@ Endpoints
|
||||
"currency": "EUR",
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"owner_ticket": null,
|
||||
"issuer": "bigevents",
|
||||
"value": "13.37"
|
||||
}
|
||||
|
||||
: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 400: The gift card could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
@@ -234,8 +205,6 @@ Endpoints
|
||||
"currency": "EUR",
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"owner_ticket": null,
|
||||
"issuer": "bigevents",
|
||||
"value": "14.00"
|
||||
}
|
||||
|
||||
@@ -281,8 +250,6 @@ Endpoints
|
||||
"testmode": false,
|
||||
"expires": null,
|
||||
"conditions": null,
|
||||
"owner_ticket": null,
|
||||
"issuer": "bigevents",
|
||||
"value": "15.37"
|
||||
}
|
||||
|
||||
@@ -326,11 +293,7 @@ Endpoints
|
||||
"value": "50.00",
|
||||
"event": "democon",
|
||||
"order": "FXQYW",
|
||||
"text": null,
|
||||
"acceptor": "bigevents",
|
||||
"info": {
|
||||
"created_by": "plugin1"
|
||||
}
|
||||
"text": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
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 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 string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
|
||||
or ``"customer"``, 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 order positions
|
||||
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||
matching easier. The parameter can be given multiple times.
|
||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, 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 ``linked_orderposition`` 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
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
@@ -138,11 +138,11 @@ Endpoints
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param id: The ``id`` field of the medium to fetch
|
||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
|
||||
or ``"customer"``, 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 order positions
|
||||
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
|
||||
matching easier. The parameter can be given multiple times.
|
||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, 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 ``linked_orderposition`` will have an attribute of the
|
||||
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
|
||||
can be given multiple times.
|
||||
: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.
|
||||
|
||||
@@ -47,8 +47,6 @@ tag string A string that i
|
||||
comment string An internal comment on the voucher
|
||||
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``.
|
||||
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.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -97,9 +95,6 @@ Endpoints
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null,
|
||||
"show_hidden_items": false,
|
||||
"all_addons_included": false,
|
||||
"all_bundles_included": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -166,10 +161,7 @@ Endpoints
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null,
|
||||
"show_hidden_items": false,
|
||||
"all_addons_included": false,
|
||||
"all_bundles_included": false
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -206,10 +198,7 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"subevent": null,
|
||||
"show_hidden_items": false,
|
||||
"all_addons_included": false,
|
||||
"all_bundles_included": false
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -236,10 +225,7 @@ Endpoints
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null,
|
||||
"show_hidden_items": false,
|
||||
"all_addons_included": false,
|
||||
"all_bundles_included": false
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a voucher for
|
||||
@@ -278,10 +264,7 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"subevent": null,
|
||||
"show_hidden_items": false,
|
||||
"all_addons_included": false,
|
||||
"all_bundles_included": false
|
||||
"subevent": null
|
||||
},
|
||||
{
|
||||
"code": "ASDKLJCYXCASDASD",
|
||||
@@ -296,10 +279,7 @@ Endpoints
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"subevent": null,
|
||||
"show_hidden_items": false,
|
||||
"all_addons_included": false,
|
||||
"all_bundles_included": false
|
||||
"subevent": null
|
||||
},
|
||||
|
||||
**Example response**:
|
||||
@@ -373,10 +353,7 @@ Endpoints
|
||||
"tag": "testvoucher",
|
||||
"comment": "",
|
||||
"seat": null,
|
||||
"subevent": null,
|
||||
"show_hidden_items": false,
|
||||
"all_addons_included": false,
|
||||
"all_bundles_included": false
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
|
||||
@@ -13,7 +13,7 @@ Core
|
||||
.. automodule:: pretix.base.signals
|
||||
: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,
|
||||
register_ticket_secret_generators, gift_card_transaction_display
|
||||
register_ticket_secret_generators
|
||||
|
||||
Order events
|
||||
""""""""""""
|
||||
@@ -21,7 +21,7 @@ Order events
|
||||
There are multiple signals that will be sent out in the ordering cycle:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
: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
|
||||
: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
|
||||
|
||||
Check-ins
|
||||
"""""""""
|
||||
|
||||
@@ -58,11 +58,11 @@ If you do not have a recent installation of ``nodejs``, install it now::
|
||||
|
||||
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
|
||||
|
||||
cd src/
|
||||
pip3 install -e ".[dev]"
|
||||
|
||||
Next, you need to copy the SCSS files from the source folder to the STATIC_ROOT directory::
|
||||
|
||||
cd src/
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
Then, create the local database::
|
||||
@@ -150,13 +150,6 @@ Add this to your ``src/pretix.cfg``::
|
||||
|
||||
Then execute ``python -m smtpd -n -c DebuggingServer localhost:1025``.
|
||||
|
||||
Working with periodic tasks
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Periodic tasks (like sendmail rules) are run when an external scheduler (like cron)
|
||||
triggers the ``runperiodic`` command.
|
||||
|
||||
To run periodic tasks, execute ``python manage.py runperiodic``.
|
||||
|
||||
Working with translations
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
If you want to translate new strings that are not yet known to the translation system,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
sphinx==7.0.*
|
||||
sphinx==6.1.*
|
||||
jinja2==3.1.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-images
|
||||
sphinxcontrib-jquery
|
||||
sphinxcontrib-spelling==8.*
|
||||
sphinxcontrib-spelling==7.*
|
||||
sphinxemoji
|
||||
pygments-markdown-lexer
|
||||
pyenchant==3.2.*
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
-e ../
|
||||
sphinx==7.0.*
|
||||
-e ../src/
|
||||
sphinx==6.1.*
|
||||
jinja2==3.1.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-images
|
||||
sphinxcontrib-jquery
|
||||
sphinxcontrib-spelling==8.*
|
||||
sphinxcontrib-spelling==7.*
|
||||
sphinxemoji
|
||||
pygments-markdown-lexer
|
||||
pyenchant==3.2.*
|
||||
|
||||
@@ -201,10 +201,6 @@ 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.
|
||||
|
||||
.. 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
|
||||
.. _SPF specification: http://www.open-spf.org/SPF_Record_Syntax
|
||||
|
||||
164
pyproject.toml
164
pyproject.toml
@@ -1,164 +0,0 @@
|
||||
[project]
|
||||
name = "pretix"
|
||||
dynamic = ["version"]
|
||||
description = "Reinventing presales, one ticket at a time"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.9"
|
||||
license = {file = "LICENSE"}
|
||||
keywords = ["tickets", "web", "shop", "ecommerce"]
|
||||
authors = [
|
||||
{name = "pretix team", email = "support@pretix.eu"},
|
||||
]
|
||||
maintainers = [
|
||||
{name = "pretix team", email = "support@pretix.eu"},
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: Other Audience",
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||
"Environment :: Web Environment",
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Framework :: Django :: 3.2",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
||||
"babel",
|
||||
"BeautifulSoup4==4.12.*",
|
||||
"bleach==5.0.*",
|
||||
"celery==5.2.*",
|
||||
"chardet==5.1.*",
|
||||
"cryptography>=3.4.2",
|
||||
"css-inline==0.8.*",
|
||||
"defusedcsv>=1.1.0",
|
||||
"dj-static",
|
||||
"Django==3.2.*,>=3.2.18",
|
||||
"django-bootstrap3==23.1.*",
|
||||
"django-compressor==4.3.*",
|
||||
"django-countries==7.5.*",
|
||||
"django-filter==23.2",
|
||||
"django-formset-js-improved==0.5.0.3",
|
||||
"django-formtools==2.4.1",
|
||||
"django-hierarkey==1.1.*",
|
||||
"django-hijack==3.3.*",
|
||||
"django-i18nfield==1.9.*,>=1.9.4",
|
||||
"django-libsass==0.9",
|
||||
"django-localflavor==4.0",
|
||||
"django-markup",
|
||||
"django-mysql",
|
||||
"django-oauth-toolkit==2.2.*",
|
||||
"django-otp==1.1.*",
|
||||
"django-phonenumber-field==7.1.*",
|
||||
"django-redis==5.2.*",
|
||||
"django-scopes==2.0.*",
|
||||
"django-statici18n==2.3.*",
|
||||
"djangorestframework==3.14.*",
|
||||
"dnspython==2.3.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
"geoip2==4.*",
|
||||
"importlib_metadata==6.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"isoweek",
|
||||
"jsonschema",
|
||||
"kombu==5.2.*",
|
||||
"libsass==0.22.*",
|
||||
"lxml",
|
||||
"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
|
||||
"mt-940==4.23.*",
|
||||
"oauthlib==3.2.*",
|
||||
"openpyxl==3.1.*",
|
||||
"packaging",
|
||||
"paypalrestsdk==1.13.*",
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.6.*",
|
||||
"phonenumberslite==8.13.*",
|
||||
"Pillow==9.5.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==4.23.*",
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
"pycparser==2.21",
|
||||
"pycryptodome==3.18.*",
|
||||
"pypdf==3.8.*",
|
||||
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.8.*",
|
||||
"python-u2flib-server==4.*",
|
||||
"pytz",
|
||||
"pyuca",
|
||||
"qrcode==7.4.*",
|
||||
"redis==4.5.*,>=4.5.4",
|
||||
"reportlab==4.0.*",
|
||||
"requests==2.30.*",
|
||||
"sentry-sdk==1.15.*",
|
||||
"sepaxml==2.6.*",
|
||||
"slimit",
|
||||
"static3==0.7.*",
|
||||
"stripe==5.4.*",
|
||||
"text-unidecode==1.*",
|
||||
"tlds>=2020041600",
|
||||
"tqdm==4.*",
|
||||
"vat_moss_forked==2020.3.20.0.11.0",
|
||||
"vobject==0.9.*",
|
||||
"webauthn==0.4.*",
|
||||
"zeep==4.2.*"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
memcached = ["pylibmc"]
|
||||
mysql = ["mysqlclient"]
|
||||
dev = [
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"flake8==6.0.*",
|
||||
"freezegun",
|
||||
"isort==5.12.*",
|
||||
"pep8-naming==0.13.*",
|
||||
"potypo",
|
||||
"pycodestyle==2.10.*",
|
||||
"pyflakes==3.0.*",
|
||||
"pytest-cache",
|
||||
"pytest-cov",
|
||||
"pytest-django==4.*",
|
||||
"pytest-mock==3.10.*",
|
||||
"pytest-rerunfailures==11.*",
|
||||
"pytest-sugar",
|
||||
"pytest-xdist==3.2.*",
|
||||
"pytest==7.3.*",
|
||||
"responses",
|
||||
]
|
||||
|
||||
[project.entry-points."distutils.commands"]
|
||||
build = "pretix._build:CustomBuild"
|
||||
build_ext = "pretix._build:CustomBuildExt"
|
||||
|
||||
[build-system]
|
||||
build-backend = "backend"
|
||||
backend-path = ["_build"]
|
||||
requires = [
|
||||
"setuptools",
|
||||
"setuptools-rust",
|
||||
"wheel",
|
||||
"importlib_metadata",
|
||||
"tomli",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://pretix.eu"
|
||||
documentation = "https://docs.pretix.eu"
|
||||
repository = "https://github.com/pretix/pretix.git"
|
||||
changelog = "https://pretix.eu/about/en/blog/"
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = true
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "pretix.__version__"}
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
include = ["pretix*"]
|
||||
namespaces = false
|
||||
40
setup.cfg
40
setup.cfg
@@ -1,40 +0,0 @@
|
||||
[check-manifest]
|
||||
ignore =
|
||||
env/**
|
||||
doc/*
|
||||
deployment/*
|
||||
res/*
|
||||
src/.update-locales
|
||||
src/Makefile
|
||||
src/manage.py
|
||||
src/pretix/icons/*
|
||||
src/pretix/static.dist/**
|
||||
src/pretix/static/jsi18n/**
|
||||
src/requirements.txt
|
||||
src/requirements/*
|
||||
src/tests/*
|
||||
src/tests/api/*
|
||||
src/tests/base/*
|
||||
src/tests/control/*
|
||||
src/tests/testdummy/*
|
||||
src/tests/templates/*
|
||||
src/tests/presale/*
|
||||
src/tests/doc/*
|
||||
src/tests/helpers/*
|
||||
src/tests/media/*
|
||||
src/tests/multidomain/*
|
||||
src/tests/plugins/*
|
||||
src/tests/plugins/badges/*
|
||||
src/tests/plugins/banktransfer/*
|
||||
src/tests/plugins/paypal/*
|
||||
src/tests/plugins/paypal2/*
|
||||
src/tests/plugins/pretixdroid/*
|
||||
src/tests/plugins/stripe/*
|
||||
src/tests/plugins/sendmail/*
|
||||
src/tests/plugins/ticketoutputpdf/*
|
||||
.*
|
||||
CODE_OF_CONDUCT.md
|
||||
CONTRIBUTING.md
|
||||
Dockerfile
|
||||
SECURITY.md
|
||||
|
||||
49
setup.py
49
setup.py
@@ -1,49 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import setuptools
|
||||
|
||||
|
||||
sys.path.append(str(Path.cwd() / 'src'))
|
||||
|
||||
|
||||
def _CustomBuild(*args, **kwargs):
|
||||
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__":
|
||||
setuptools.setup(
|
||||
cmdclass=cmdclass,
|
||||
)
|
||||
33
src/MANIFEST.in
Normal file
33
src/MANIFEST.in
Normal file
@@ -0,0 +1,33 @@
|
||||
include LICENSE
|
||||
include README.rst
|
||||
global-include *.proto
|
||||
recursive-include pretix/static *
|
||||
recursive-include pretix/static.dist *
|
||||
recursive-include pretix/locale *
|
||||
recursive-include pretix/helpers/locale *
|
||||
recursive-include pretix/base/templates *
|
||||
recursive-include pretix/control/templates *
|
||||
recursive-include pretix/presale/templates *
|
||||
recursive-include pretix/plugins/banktransfer/templates *
|
||||
recursive-include pretix/plugins/banktransfer/static *
|
||||
recursive-include pretix/plugins/manualpayment/templates *
|
||||
recursive-include pretix/plugins/manualpayment/static *
|
||||
recursive-include pretix/plugins/paypal/templates *
|
||||
recursive-include pretix/plugins/paypal/static *
|
||||
recursive-include pretix/plugins/paypal2/templates *
|
||||
recursive-include pretix/plugins/paypal2/static *
|
||||
recursive-include pretix/plugins/pretixdroid/templates *
|
||||
recursive-include pretix/plugins/pretixdroid/static *
|
||||
recursive-include pretix/plugins/sendmail/templates *
|
||||
recursive-include pretix/plugins/statistics/templates *
|
||||
recursive-include pretix/plugins/statistics/static *
|
||||
recursive-include pretix/plugins/stripe/templates *
|
||||
recursive-include pretix/plugins/stripe/static *
|
||||
recursive-include pretix/plugins/ticketoutputpdf/templates *
|
||||
recursive-include pretix/plugins/ticketoutputpdf/static *
|
||||
recursive-include pretix/plugins/badges/templates *
|
||||
recursive-include pretix/plugins/badges/static *
|
||||
recursive-include pretix/plugins/returnurl/templates *
|
||||
recursive-include pretix/plugins/returnurl/static *
|
||||
recursive-include pretix/plugins/webcheckin/templates *
|
||||
recursive-include pretix/plugins/webcheckin/static *
|
||||
@@ -19,4 +19,4 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "4.20.4"
|
||||
__version__ = "4.19.0.dev0"
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
import django.conf.locale
|
||||
from pycountry import currencies
|
||||
|
||||
from django.utils.translation import gettext_lazy as _ # NOQA
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.humanize',
|
||||
'pretix.base',
|
||||
'pretix.control',
|
||||
'pretix.presale',
|
||||
'pretix.multidomain',
|
||||
'pretix.api',
|
||||
'pretix.helpers',
|
||||
'rest_framework',
|
||||
'djangoformsetjs',
|
||||
'compressor',
|
||||
'bootstrap3',
|
||||
'pretix.plugins.banktransfer',
|
||||
'pretix.plugins.stripe',
|
||||
'pretix.plugins.paypal',
|
||||
'pretix.plugins.paypal2',
|
||||
'pretix.plugins.ticketoutputpdf',
|
||||
'pretix.plugins.sendmail',
|
||||
'pretix.plugins.statistics',
|
||||
'pretix.plugins.reports',
|
||||
'pretix.plugins.checkinlists',
|
||||
'pretix.plugins.pretixdroid',
|
||||
'pretix.plugins.badges',
|
||||
'pretix.plugins.manualpayment',
|
||||
'pretix.plugins.returnurl',
|
||||
'pretix.plugins.webcheckin',
|
||||
'django_countries',
|
||||
'oauth2_provider',
|
||||
'phonenumber_field',
|
||||
'statici18n',
|
||||
]
|
||||
|
||||
FORMAT_MODULE_PATH = [
|
||||
'pretix.helpers.formats',
|
||||
]
|
||||
|
||||
ALL_LANGUAGES = [
|
||||
('en', _('English')),
|
||||
('de', _('German')),
|
||||
('de-informal', _('German (informal)')),
|
||||
('ar', _('Arabic')),
|
||||
('zh-hans', _('Chinese (simplified)')),
|
||||
('cs', _('Czech')),
|
||||
('da', _('Danish')),
|
||||
('nl', _('Dutch')),
|
||||
('nl-informal', _('Dutch (informal)')),
|
||||
('fr', _('French')),
|
||||
('fi', _('Finnish')),
|
||||
('gl', _('Galician')),
|
||||
('el', _('Greek')),
|
||||
('it', _('Italian')),
|
||||
('lv', _('Latvian')),
|
||||
('pl', _('Polish')),
|
||||
('pt-pt', _('Portuguese (Portugal)')),
|
||||
('pt-br', _('Portuguese (Brazil)')),
|
||||
('ro', _('Romanian')),
|
||||
('ru', _('Russian')),
|
||||
('es', _('Spanish')),
|
||||
('tr', _('Turkish')),
|
||||
('uk', _('Ukrainian')),
|
||||
]
|
||||
LANGUAGES_OFFICIAL = {
|
||||
'en', 'de', 'de-informal'
|
||||
}
|
||||
LANGUAGES_RTL = {
|
||||
'ar', 'hw'
|
||||
}
|
||||
LANGUAGES_INCUBATING = {
|
||||
'pl', 'fi', 'pt-br', 'gl',
|
||||
}
|
||||
LOCALE_PATHS = [
|
||||
os.path.join(os.path.dirname(__file__), 'locale'),
|
||||
]
|
||||
|
||||
EXTRA_LANG_INFO = {
|
||||
'de-informal': {
|
||||
'bidi': False,
|
||||
'code': 'de-informal',
|
||||
'name': 'German (informal)',
|
||||
'name_local': 'Deutsch',
|
||||
'public_code': 'de',
|
||||
},
|
||||
'nl-informal': {
|
||||
'bidi': False,
|
||||
'code': 'nl-informal',
|
||||
'name': 'Dutch (informal)',
|
||||
'name_local': 'Nederlands',
|
||||
'public_code': 'nl',
|
||||
},
|
||||
'fr': {
|
||||
'bidi': False,
|
||||
'code': 'fr',
|
||||
'name': 'French',
|
||||
'name_local': 'Français'
|
||||
},
|
||||
'lv': {
|
||||
'bidi': False,
|
||||
'code': 'lv',
|
||||
'name': 'Latvian',
|
||||
'name_local': 'Latviešu'
|
||||
},
|
||||
'pt-pt': {
|
||||
'bidi': False,
|
||||
'code': 'pt-pt',
|
||||
'name': 'Portuguese',
|
||||
'name_local': 'Português',
|
||||
},
|
||||
}
|
||||
|
||||
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
|
||||
|
||||
template_loaders = (
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'pretix.helpers.template_loaders.AppLoader',
|
||||
)
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
],
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.i18n',
|
||||
'django.template.context_processors.media',
|
||||
"django.template.context_processors.request",
|
||||
'django.template.context_processors.static',
|
||||
'django.template.context_processors.tz',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'pretix.base.context.contextprocessor',
|
||||
'pretix.control.context.contextprocessor',
|
||||
'pretix.presale.context.contextprocessor',
|
||||
],
|
||||
'loaders': template_loaders
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
STATIC_ROOT = os.path.join(os.path.dirname(__file__), 'static.dist')
|
||||
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
'compressor.finders.CompressorFinder',
|
||||
)
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, 'pretix/static')
|
||||
] if os.path.exists(os.path.join(BASE_DIR, 'pretix/static')) else []
|
||||
|
||||
STATICI18N_ROOT = os.path.join(BASE_DIR, "pretix/static")
|
||||
|
||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
||||
|
||||
# if os.path.exists(os.path.join(DATA_DIR, 'static')):
|
||||
# STATICFILES_DIRS.insert(0, os.path.join(DATA_DIR, 'static'))
|
||||
|
||||
COMPRESS_PRECOMPILERS = (
|
||||
('text/x-scss', 'django_libsass.SassCompiler'),
|
||||
('text/vue', 'pretix.helpers.compressor.VueCompiler'),
|
||||
)
|
||||
|
||||
COMPRESS_OFFLINE_CONTEXT = {
|
||||
'basetpl': 'empty.html',
|
||||
}
|
||||
|
||||
COMPRESS_ENABLED = True
|
||||
COMPRESS_OFFLINE = True
|
||||
|
||||
COMPRESS_FILTERS = {
|
||||
'css': (
|
||||
# CssAbsoluteFilter is incredibly slow, especially when dealing with our _flags.scss
|
||||
# However, we don't need it if we consequently use the static() function in Sass
|
||||
# 'compressor.filters.css_default.CssAbsoluteFilter',
|
||||
'compressor.filters.cssmin.rCSSMinFilter',
|
||||
),
|
||||
'js': (
|
||||
'compressor.filters.jsmin.JSMinFilter',
|
||||
)
|
||||
}
|
||||
|
||||
CURRENCIES = list(currencies)
|
||||
CURRENCY_PLACES = {
|
||||
# default is 2
|
||||
'BIF': 0,
|
||||
'CLP': 0,
|
||||
'DJF': 0,
|
||||
'GNF': 0,
|
||||
'JPY': 0,
|
||||
'KMF': 0,
|
||||
'KRW': 0,
|
||||
'MGA': 0,
|
||||
'PYG': 0,
|
||||
'RWF': 0,
|
||||
'VND': 0,
|
||||
'VUV': 0,
|
||||
'XAF': 0,
|
||||
'XOF': 0,
|
||||
'XPF': 0,
|
||||
}
|
||||
|
||||
PRETIX_EMAIL_NONE_VALUE = 'none@well-known.pretix.eu'
|
||||
PRETIX_PRIMARY_COLOR = '#8E44B3'
|
||||
|
||||
# pretix includes caching options for some special situations where full HTML responses are cached. This might be
|
||||
# stressful for some cache setups so it is enabled by default and currently can't be enabled through pretix.cfg
|
||||
CACHE_LARGE_VALUES_ALLOWED = False
|
||||
CACHE_LARGE_VALUES_ALIAS = 'default'
|
||||
|
||||
# Allowed file extensions for various places plus matching Pillow formats.
|
||||
# Never allow EPS, it is full of dangerous bugs.
|
||||
FILE_UPLOAD_EXTENSIONS_IMAGE = (".png", ".jpg", ".gif", ".jpeg")
|
||||
PILLOW_FORMATS_IMAGE = ('PNG', 'GIF', 'JPEG')
|
||||
|
||||
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", "jpg", ".gif", ".jpeg")
|
||||
|
||||
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", "jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
|
||||
PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF')
|
||||
|
||||
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
||||
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
|
||||
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
|
||||
".bmp", ".tif", ".tiff"
|
||||
)
|
||||
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
|
||||
@@ -1,82 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from setuptools.command.build import build
|
||||
from setuptools.command.build_ext import build_ext
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
npm_installed = False
|
||||
|
||||
|
||||
def npm_install():
|
||||
global npm_installed
|
||||
|
||||
if not npm_installed:
|
||||
# keep this in sync with Makefile!
|
||||
node_prefix = os.path.join(here, 'static.dist', 'node_prefix')
|
||||
os.makedirs(node_prefix, exist_ok=True)
|
||||
shutil.copytree(os.path.join(here, 'static', 'npm_dir'), node_prefix, dirs_exist_ok=True)
|
||||
subprocess.check_call('npm install', shell=True, cwd=node_prefix)
|
||||
npm_installed = True
|
||||
|
||||
|
||||
class CustomBuild(build):
|
||||
def run(self):
|
||||
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
|
||||
# Only run this command on the pretix module, not on other modules even if it's registered globally
|
||||
# in some cases
|
||||
return build.run(self)
|
||||
if "PRETIX_DOCKER_BUILD" in os.environ:
|
||||
return # this is a hack to allow calling this file early in our docker build to make use of caching
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix._build_settings")
|
||||
os.environ.setdefault("PRETIX_IGNORE_CONFLICTS", "True")
|
||||
import django
|
||||
django.setup()
|
||||
from django.conf import settings
|
||||
from django.core import management
|
||||
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_OFFLINE = True
|
||||
|
||||
npm_install()
|
||||
management.call_command('compilemessages', verbosity=1)
|
||||
management.call_command('compilejsi18n', verbosity=1)
|
||||
management.call_command('collectstatic', verbosity=1, interactive=False)
|
||||
management.call_command('compress', verbosity=1)
|
||||
|
||||
build.run(self)
|
||||
|
||||
|
||||
class CustomBuildExt(build_ext):
|
||||
def run(self):
|
||||
if "src" not in os.listdir(".") or "pretix" not in os.listdir("src"):
|
||||
# Only run this command on the pretix module, not on other modules even if it's registered globally
|
||||
# in some cases
|
||||
return build_ext.run(self)
|
||||
if "PRETIX_DOCKER_BUILD" in os.environ:
|
||||
return # this is a hack to allow calling this file early in our docker build to make use of caching
|
||||
npm_install()
|
||||
build_ext.run(self)
|
||||
@@ -1,48 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
This file contains settings that we need at wheel require time. All settings that we only need at runtime are set
|
||||
in settings.py.
|
||||
"""
|
||||
from ._base_settings import * # NOQA
|
||||
|
||||
ENTROPY = {
|
||||
'order_code': 5,
|
||||
'customer_identifier': 7,
|
||||
'ticket_secret': 32,
|
||||
'voucher_code': 16,
|
||||
'giftcard_secret': 12,
|
||||
}
|
||||
|
||||
MAIL_FROM_ORGANIZERS = 'invalid@invalid'
|
||||
FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT = 10
|
||||
FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT = 10
|
||||
FILE_UPLOAD_MAX_SIZE_IMAGE = 10
|
||||
DEFAULT_CURRENCY = 'EUR'
|
||||
SECRET_KEY = "build-time-secret-key"
|
||||
HAS_REDIS = False
|
||||
STATIC_URL = '/static/'
|
||||
HAS_MEMCACHED = False
|
||||
HAS_CELERY = False
|
||||
HAS_GEOIP = False
|
||||
SENTRY_ENABLED = False
|
||||
@@ -201,7 +201,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('DELETE', 'api-v1:cartposition-detail'),
|
||||
('GET', 'api-v1:giftcard-list'),
|
||||
('POST', 'api-v1:giftcard-transact'),
|
||||
('PATCH', 'api-v1:giftcard-detail'),
|
||||
('GET', 'plugins:pretix_posbackend:posclosing-list'),
|
||||
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
|
||||
('POST', 'plugins:pretix_posbackend:posclosing-list'),
|
||||
|
||||
@@ -19,30 +19,3 @@
|
||||
# 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 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,9 +50,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import (
|
||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||
)
|
||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||
from pretix.base.services.seating import (
|
||||
SeatProtected, generate_seats, validate_plan_change,
|
||||
)
|
||||
@@ -685,7 +683,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'waiting_list_phones_asked',
|
||||
'waiting_list_phones_required',
|
||||
'waiting_list_phones_explanation_text',
|
||||
'waiting_list_limit_per_user',
|
||||
'max_items_per_order',
|
||||
'reservation_time',
|
||||
'contact_mail',
|
||||
@@ -768,7 +765,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'invoice_footer_text',
|
||||
'invoice_eu_currencies',
|
||||
'invoice_logo_image',
|
||||
'invoice_renderer_highlight_order_code',
|
||||
'cancel_allow_user',
|
||||
'cancel_allow_user_until',
|
||||
'cancel_allow_user_unpaid_keep',
|
||||
@@ -906,23 +902,3 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
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,9 +64,7 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
|
||||
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)
|
||||
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True)
|
||||
else:
|
||||
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
|
||||
required=False,
|
||||
|
||||
@@ -22,14 +22,12 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers import AsymmetricField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import CompatibleJSONField
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
@@ -37,8 +35,8 @@ from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||
MembershipType, OrderPosition, Organizer, ReusableMedium, SeatingPlan,
|
||||
Team, TeamAPIToken, TeamInvite, User,
|
||||
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
||||
User,
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
@@ -129,52 +127,8 @@ class MembershipSerializer(I18nAwareModelSerializer):
|
||||
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):
|
||||
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):
|
||||
data = super().validate(data)
|
||||
@@ -197,8 +151,7 @@ class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',
|
||||
'issuer')
|
||||
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions')
|
||||
|
||||
|
||||
class OrderEventSlugField(serializers.RelatedField):
|
||||
@@ -209,12 +162,11 @@ class OrderEventSlugField(serializers.RelatedField):
|
||||
|
||||
class GiftCardTransactionSerializer(I18nAwareModelSerializer):
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
model = GiftCardTransaction
|
||||
fields = ('id', 'datetime', 'value', 'event', 'order', 'text', 'info', 'acceptor')
|
||||
fields = ('id', 'datetime', 'value', 'event', 'order', 'text')
|
||||
|
||||
|
||||
class EventSlugField(serializers.SlugRelatedField):
|
||||
|
||||
@@ -63,8 +63,7 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
||||
model = Voucher
|
||||
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
|
||||
'all_bundles_included')
|
||||
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat')
|
||||
read_only_fields = ('id', 'redeemed')
|
||||
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.update(data)
|
||||
|
||||
WaitingListEntry.clean_duplicate(event, full_data.get('email'), full_data.get('item'), full_data.get('variation'),
|
||||
WaitingListEntry.clean_duplicate(full_data.get('email'), full_data.get('item'), full_data.get('variation'),
|
||||
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_subevent(event, full_data.get('subevent'))
|
||||
|
||||
@@ -89,7 +89,6 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
|
||||
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
|
||||
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
|
||||
|
||||
checkinlist_router = routers.DefaultRouter()
|
||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
||||
|
||||
@@ -562,9 +562,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
||||
}, status=400)
|
||||
else:
|
||||
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())
|
||||
op_candidates = [media.linked_orderposition] + 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
|
||||
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
|
||||
|
||||
@@ -47,13 +47,11 @@ from pretix.api.auth.permission import EventCRUDPermission
|
||||
from pretix.api.pagination import TotalOrderingFilter
|
||||
from pretix.api.serializers.event import (
|
||||
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
||||
EventSettingsSerializer, ItemMetaPropertiesSerializer, SubEventSerializer,
|
||||
TaxRuleSerializer,
|
||||
EventSettingsSerializer, SubEventSerializer, TaxRuleSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
CartPosition, Device, Event, ItemMetaProperty, SeatCategoryMapping,
|
||||
TaxRule, TeamAPIToken,
|
||||
CartPosition, Device, Event, SeatCategoryMapping, TaxRule, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
@@ -524,54 +522,6 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
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):
|
||||
permission = None
|
||||
write_permission = 'can_change_event_settings'
|
||||
|
||||
@@ -26,7 +26,6 @@ from decimal import Decimal
|
||||
|
||||
import django_filters
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
|
||||
@@ -68,8 +67,8 @@ from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
|
||||
Invoice, InvoiceAddress, ItemMetaValue, ItemVariation,
|
||||
ItemVariationMetaValue, Order, OrderFee, OrderPayment, OrderPosition,
|
||||
OrderRefund, Quota, ReusableMedium, SubEvent, SubEventMetaValue, TaxRule,
|
||||
TeamAPIToken, generate_secret,
|
||||
OrderRefund, Quota, SubEvent, SubEventMetaValue, TaxRule, TeamAPIToken,
|
||||
generate_secret,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret,
|
||||
@@ -149,13 +148,9 @@ with scopes_disabled():
|
||||
else:
|
||||
code = Q(code__icontains=Order.normalize_code(u))
|
||||
|
||||
invoice_nos = {u, u.upper()}
|
||||
if u.isdigit():
|
||||
for i in range(2, 12):
|
||||
invoice_nos.add(u.zfill(i))
|
||||
|
||||
matching_invoices = Invoice.objects.filter(
|
||||
Q(invoice_no__in=invoice_nos)
|
||||
Q(invoice_no__iexact=u)
|
||||
| Q(invoice_no__iexact=u.zfill(5))
|
||||
| Q(full_invoice_no__iexact=u)
|
||||
).values_list('order_id', flat=True)
|
||||
|
||||
@@ -167,15 +162,12 @@ with scopes_disabled():
|
||||
)
|
||||
).values('id')
|
||||
|
||||
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderposition__order_id', flat=True)
|
||||
|
||||
mainq = (
|
||||
code
|
||||
| Q(email__icontains=u)
|
||||
| Q(invoice_address__name_cached__icontains=u)
|
||||
| Q(invoice_address__company__icontains=u)
|
||||
| Q(pk__in=matching_invoices)
|
||||
| Q(pk__in=matching_media)
|
||||
| Q(comment__icontains=u)
|
||||
| Q(has_pos=True)
|
||||
)
|
||||
@@ -322,7 +314,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
@action(detail=True, methods=['POST'])
|
||||
def mark_paid(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
send_mail = request.data.get('send_email', True) if request.data else True
|
||||
send_mail = request.data.get('send_email', True)
|
||||
|
||||
if order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
|
||||
|
||||
@@ -381,7 +373,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def mark_canceled(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True) if request.data else True
|
||||
send_mail = request.data.get('send_email', True)
|
||||
comment = request.data.get('comment', None)
|
||||
cancellation_fee = request.data.get('cancellation_fee', None)
|
||||
if cancellation_fee:
|
||||
@@ -440,7 +432,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def approve(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True) if request.data else True
|
||||
send_mail = request.data.get('send_email', True)
|
||||
|
||||
order = self.get_object()
|
||||
try:
|
||||
@@ -458,7 +450,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def deny(self, request, **kwargs):
|
||||
send_mail = request.data.get('send_email', True) if request.data else True
|
||||
send_mail = request.data.get('send_email', True)
|
||||
comment = request.data.get('comment', '')
|
||||
|
||||
order = self.get_object()
|
||||
@@ -928,7 +920,6 @@ with scopes_disabled():
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
matching_media = ReusableMedium.objects.filter(identifier=value).values_list('linked_orderposition', flat=True)
|
||||
return queryset.filter(
|
||||
Q(secret__istartswith=value)
|
||||
| Q(attendee_name_cached__icontains=value)
|
||||
@@ -938,7 +929,6 @@ with scopes_disabled():
|
||||
| Q(order__code__istartswith=value)
|
||||
| Q(order__invoice_address__name_cached__icontains=value)
|
||||
| Q(order__email__icontains=value)
|
||||
| Q(pk__in=matching_media)
|
||||
)
|
||||
|
||||
def has_checkin_qs(self, queryset, name, value):
|
||||
@@ -1182,7 +1172,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
||||
ftype, ignored = mimetypes.guess_type(image_file.name)
|
||||
extension = os.path.basename(image_file.name).split('.')[-1]
|
||||
else:
|
||||
img = Image.open(image_file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||
img = Image.open(image_file)
|
||||
ftype = Image.MIME[img.format]
|
||||
extensions = {
|
||||
'GIF': 'gif', 'TIFF': 'tif', 'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'
|
||||
@@ -1454,7 +1444,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
return order.payments.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
send_mail = request.data.get('send_email', True) if request.data else True
|
||||
send_mail = request.data.get('send_email', True)
|
||||
serializer = OrderPaymentCreateSerializer(data=request.data, context=self.get_serializer_context())
|
||||
serializer.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
@@ -1499,7 +1489,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
def confirm(self, request, **kwargs):
|
||||
payment = self.get_object()
|
||||
force = request.data.get('force', False)
|
||||
send_mail = request.data.get('send_email', True) if request.data else True
|
||||
send_mail = request.data.get('send_email', True)
|
||||
|
||||
if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
|
||||
return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -155,9 +155,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
qs = self.request.organizer.accepted_gift_cards
|
||||
else:
|
||||
qs = self.request.organizer.issued_gift_cards.all()
|
||||
return qs.prefetch_related(
|
||||
'issuer'
|
||||
)
|
||||
return qs
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
@@ -168,7 +166,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
def perform_create(self, serializer):
|
||||
value = serializer.validated_data.pop('value')
|
||||
inst = serializer.save(issuer=self.request.organizer)
|
||||
inst.transactions.create(value=value, acceptor=self.request.organizer)
|
||||
inst.transactions.create(value=value)
|
||||
inst.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
@@ -181,32 +179,18 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
if 'include_accepted' in self.request.GET:
|
||||
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)
|
||||
|
||||
value = serializer.validated_data.pop('value', None)
|
||||
|
||||
if any(k != 'value' for k in self.request.data):
|
||||
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
|
||||
testmode=serializer.instance.testmode)
|
||||
inst.log_action(
|
||||
'pretix.giftcards.modified',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
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}
|
||||
)
|
||||
|
||||
old_value = serializer.instance.value
|
||||
value = serializer.validated_data.pop('value')
|
||||
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
|
||||
testmode=serializer.instance.testmode)
|
||||
diff = value - old_value
|
||||
inst.transactions.create(value=diff)
|
||||
inst.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'value': diff}
|
||||
)
|
||||
return inst
|
||||
|
||||
@action(detail=True, methods=["POST"])
|
||||
@@ -219,21 +203,18 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
|
||||
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'):
|
||||
return Response({
|
||||
'value': ['The gift card does not have sufficient credit for this operation.']
|
||||
}, status=status.HTTP_409_CONFLICT)
|
||||
gc.transactions.create(value=value, text=text, info=info, acceptor=self.request.organizer)
|
||||
gc.transactions.create(value=value, text=text)
|
||||
gc.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'value': value, 'text': text}
|
||||
)
|
||||
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
|
||||
return Response(GiftCardSerializer(gc).data, status=status.HTTP_200_OK)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
raise MethodNotAllowed("Gift cards cannot be deleted.")
|
||||
@@ -254,7 +235,7 @@ class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
return get_object_or_404(qs, pk=self.kwargs.get('giftcard'))
|
||||
|
||||
def get_queryset(self):
|
||||
return self.giftcard.transactions.select_related('order', 'order__event').prefetch_related('acceptor')
|
||||
return self.giftcard.transactions.select_related('order', 'order__event')
|
||||
|
||||
|
||||
class TeamViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@@ -117,15 +117,13 @@ def oidc_validate_and_complete_config(config):
|
||||
scopes=", ".join(provider_config.get("scopes_supported", []))
|
||||
))
|
||||
|
||||
if "claims_supported" in provider_config:
|
||||
claims_supported = provider_config.get("claims_supported", [])
|
||||
for k, v in config.items():
|
||||
if k.endswith('_field') and v:
|
||||
if v not in claims_supported: # https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||
raise ValidationError(_('You are requesting field "{field}" but provider only supports these: {fields}.').format(
|
||||
field=v,
|
||||
fields=", ".join(provider_config.get("claims_supported", []))
|
||||
))
|
||||
for k, v in config.items():
|
||||
if k.endswith('_field') and v:
|
||||
if v not in provider_config.get("claims_supported", []): # https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||
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
|
||||
return config
|
||||
|
||||
@@ -58,7 +58,6 @@ class EventDataExporter(ListExporter):
|
||||
_("Short form"),
|
||||
_("Shop is live"),
|
||||
_("Event currency"),
|
||||
_("Timezone"),
|
||||
_("Event start time"),
|
||||
_("Event end time"),
|
||||
_("Admission time"),
|
||||
@@ -76,18 +75,16 @@ class EventDataExporter(ListExporter):
|
||||
|
||||
for e in self.events.all():
|
||||
m = e.meta_data
|
||||
tz = e.timezone
|
||||
yield [
|
||||
str(e.name),
|
||||
e.slug,
|
||||
_('Yes') if e.live else _('No'),
|
||||
e.currency,
|
||||
str(e.timezone),
|
||||
date_format(e.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
|
||||
date_format(e.date_to.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.date_to else '',
|
||||
date_format(e.date_admission.astimezone(tz), 'SHORT_DATETIME_FORMAT') if e.date_admission 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 '',
|
||||
date_format(e.date_from, 'SHORT_DATETIME_FORMAT'),
|
||||
date_format(e.date_to, 'SHORT_DATETIME_FORMAT') if e.date_to else '',
|
||||
date_format(e.date_admission, 'SHORT_DATETIME_FORMAT') if e.date_admission else '',
|
||||
date_format(e.presale_start, 'SHORT_DATETIME_FORMAT') if e.presale_start else '',
|
||||
date_format(e.presale_end, 'SHORT_DATETIME_FORMAT') if e.presale_end else '',
|
||||
str(e.location),
|
||||
e.geo_lat or '',
|
||||
e.geo_lon or '',
|
||||
@@ -97,7 +94,7 @@ class EventDataExporter(ListExporter):
|
||||
]
|
||||
|
||||
def get_filename(self):
|
||||
return '{}_events'.format(self.organizer.slug)
|
||||
return '{}_events'.format(self.events.first().organizer.slug)
|
||||
|
||||
|
||||
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_eventdata")
|
||||
|
||||
@@ -103,9 +103,7 @@ class InvoiceExporterMixin:
|
||||
qs = qs.annotate(
|
||||
has_payment_with_provider=Exists(
|
||||
OrderPayment.objects.filter(
|
||||
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),
|
||||
Q(order=OuterRef('order_id')) & Q(provider=form_data.get('payment_provider'))
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -157,7 +155,7 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
|
||||
self.progress_callback(counter / total * 100)
|
||||
|
||||
if self.is_multievent:
|
||||
filename = '{}_invoices.zip'.format(self.organizer.slug)
|
||||
filename = '{}_invoices.zip'.format(self.events.first().organizer.slug)
|
||||
else:
|
||||
filename = '{}_invoices.zip'.format(self.event.slug)
|
||||
|
||||
@@ -417,7 +415,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
||||
|
||||
def get_filename(self):
|
||||
if self.is_multievent:
|
||||
return '{}_invoices'.format(self.organizer.slug)
|
||||
return '{}_invoices'.format(self.events.first().organizer.slug)
|
||||
else:
|
||||
return '{}_invoices'.format(self.event.slug)
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ class ItemDataExporter(ListExporter):
|
||||
|
||||
def get_filename(self):
|
||||
if self.is_multievent:
|
||||
return '{}_products'.format(self.organizer.slug)
|
||||
return '{}_products'.format(self.events.first().organizer.slug)
|
||||
return '{}_products'.format(self.event.slug)
|
||||
|
||||
def prepare_xlsx_sheet(self, ws):
|
||||
|
||||
@@ -63,7 +63,7 @@ class MailExporter(BaseExporter):
|
||||
| set(a['attendee_email'] for a in pos if a['attendee_email']))
|
||||
|
||||
if self.is_multievent:
|
||||
return '{}_pretixemails.txt'.format(self.organizer.slug), 'text/plain', data.encode("utf-8")
|
||||
return '{}_pretixemails.txt'.format(self.events.first().organizer.slug), 'text/plain', data.encode("utf-8")
|
||||
else:
|
||||
return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8")
|
||||
|
||||
|
||||
@@ -49,24 +49,18 @@ from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.translation import (
|
||||
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 (
|
||||
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
||||
OrderPosition, Question,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
OrderFee, OrderPayment, OrderRefund, Transaction,
|
||||
)
|
||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
|
||||
|
||||
from ...control.forms.filter import get_all_payment_providers
|
||||
from ...helpers import GroupConcat
|
||||
from ...helpers.iter import chunked_iterable
|
||||
from ...helpers.safe_openpyxl import remove_invalid_excel_chars
|
||||
from ..exporter import (
|
||||
ListExporter, MultiSheetListExporter, OrganizerLevelExportMixin,
|
||||
)
|
||||
@@ -694,8 +688,8 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
|
||||
row += [
|
||||
_('Yes') if op.blocked else '',
|
||||
date_format(op.valid_from.astimezone(tz), 'SHORT_DATETIME_FORMAT') if op.valid_from else '',
|
||||
date_format(op.valid_until.astimezone(tz), 'SHORT_DATETIME_FORMAT') if op.valid_until else '',
|
||||
date_format(op.valid_from, 'SHORT_DATETIME_FORMAT') if op.valid_from else '',
|
||||
date_format(op.valid_until, 'SHORT_DATETIME_FORMAT') if op.valid_until else '',
|
||||
]
|
||||
row.append(order.comment)
|
||||
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
||||
@@ -760,186 +754,11 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
|
||||
def get_filename(self):
|
||||
if self.is_multievent:
|
||||
return '{}_orders'.format(self.organizer.slug)
|
||||
return '{}_orders'.format(self.events.first().organizer.slug)
|
||||
else:
|
||||
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):
|
||||
identifier = 'paymentlist'
|
||||
verbose_name = gettext_lazy('Payments and refunds')
|
||||
@@ -1061,7 +880,7 @@ class PaymentListExporter(ListExporter):
|
||||
|
||||
def get_filename(self):
|
||||
if self.is_multievent:
|
||||
return '{}_payments'.format(self.organizer.slug)
|
||||
return '{}_payments'.format(self.events.first().organizer.slug)
|
||||
else:
|
||||
return '{}_payments'.format(self.event.slug)
|
||||
|
||||
@@ -1218,7 +1037,7 @@ class GiftcardRedemptionListExporter(ListExporter):
|
||||
|
||||
def get_filename(self):
|
||||
if self.is_multievent:
|
||||
return '{}_giftcardredemptions'.format(self.organizer.slug)
|
||||
return '{}_giftcardredemptions'.format(self.events.first().organizer.slug)
|
||||
else:
|
||||
return '{}_giftcardredemptions'.format(self.event.slug)
|
||||
|
||||
@@ -1346,16 +1165,6 @@ def register_multievent_orderlist_exporter(sender, **kwargs):
|
||||
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")
|
||||
def register_paymentlist_exporter(sender, **kwargs):
|
||||
return PaymentListExporter
|
||||
|
||||
@@ -45,7 +45,6 @@ import pytz
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.gis.geoip2 import GeoIP2
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.validators import (
|
||||
@@ -92,7 +91,6 @@ from pretix.helpers.countries import (
|
||||
CachedCountries, get_phone_prefixes_sorted_and_localized,
|
||||
)
|
||||
from pretix.helpers.escapejson import escapejson_attr
|
||||
from pretix.helpers.http import get_client_ip
|
||||
from pretix.helpers.i18n import get_format_without_seconds
|
||||
from pretix.presale.signals import question_form_fields
|
||||
|
||||
@@ -353,15 +351,6 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
||||
return ""
|
||||
|
||||
|
||||
def guess_country_from_request(request, event):
|
||||
if settings.HAS_GEOIP:
|
||||
g = GeoIP2()
|
||||
res = g.country(get_client_ip(request))
|
||||
if res['country_code'] and len(res['country_code']) == 2:
|
||||
return Country(res['country_code'])
|
||||
return guess_country(event)
|
||||
|
||||
|
||||
def guess_country(event):
|
||||
# Try to guess the initial country from either the country of the merchant
|
||||
# or the locale. This will hopefully save at least some users some scrolling :)
|
||||
@@ -393,12 +382,6 @@ def guess_phone_prefix(event):
|
||||
return get_phone_prefix(country)
|
||||
|
||||
|
||||
def guess_phone_prefix_from_request(request, event):
|
||||
with language(get_babel_locale()):
|
||||
country = str(guess_country_from_request(request, event))
|
||||
return get_phone_prefix(country)
|
||||
|
||||
|
||||
def get_phone_prefix(country):
|
||||
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
||||
if country in values:
|
||||
@@ -496,14 +479,14 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
||||
file = BytesIO(data['content'])
|
||||
|
||||
try:
|
||||
image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||
image = Image.open(file)
|
||||
# verify() must be called immediately after the constructor.
|
||||
image.verify()
|
||||
|
||||
# We want to do more than just verify(), so we need to re-open the file
|
||||
if hasattr(file, 'seek'):
|
||||
file.seek(0)
|
||||
image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||
image = Image.open(file)
|
||||
|
||||
# load() is a potential DoS vector (see Django bug #18520), so we verify the size first
|
||||
if image.width > 10_000 or image.height > 10_000:
|
||||
@@ -562,7 +545,7 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
||||
return f
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('ext_whitelist', settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE)
|
||||
kwargs.setdefault('ext_whitelist', (".png", ".jpg", ".jpeg", ".jfif", ".tif", ".tiff", ".bmp"))
|
||||
kwargs.setdefault('max_size', settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -573,7 +556,6 @@ class BaseQuestionsForm(forms.Form):
|
||||
the attendee name for admission tickets, if the corresponding setting is enabled,
|
||||
as well as additional questions defined by the organizer.
|
||||
"""
|
||||
address_validation = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
@@ -582,7 +564,6 @@ class BaseQuestionsForm(forms.Form):
|
||||
:param cartpos: The cart position the form should be for
|
||||
:param event: The event this belongs to
|
||||
"""
|
||||
request = kwargs.pop('request', None)
|
||||
cartpos = self.cartpos = kwargs.pop('cartpos', None)
|
||||
orderpos = self.orderpos = kwargs.pop('orderpos', None)
|
||||
pos = cartpos or orderpos
|
||||
@@ -680,7 +661,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
'autocomplete': 'address-level2',
|
||||
}),
|
||||
)
|
||||
country = (cartpos.country if cartpos else orderpos.country) or guess_country_from_request(request, event)
|
||||
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
|
||||
add_fields['country'] = CountryField(
|
||||
countries=CachedCountries
|
||||
).formfield(
|
||||
@@ -787,7 +768,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
help_text=help_text,
|
||||
widget=forms.Select,
|
||||
empty_label=' ',
|
||||
initial=initial.answer if initial else (guess_country_from_request(request, event) if required else None),
|
||||
initial=initial.answer if initial else (guess_country(event) if required else None),
|
||||
)
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
field = forms.ModelChoiceField(
|
||||
@@ -822,7 +803,11 @@ class BaseQuestionsForm(forms.Form):
|
||||
help_text=help_text,
|
||||
initial=initial.file if initial else None,
|
||||
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_OTHER,
|
||||
ext_whitelist=(
|
||||
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
|
||||
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
|
||||
".bmp", ".tif", ".tiff"
|
||||
),
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER,
|
||||
)
|
||||
elif q.type == Question.TYPE_DATE:
|
||||
@@ -871,7 +856,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
initial = None
|
||||
|
||||
if not initial:
|
||||
phone_prefix = guess_phone_prefix_from_request(request, event)
|
||||
phone_prefix = guess_phone_prefix(event)
|
||||
if phone_prefix:
|
||||
initial = "+{}.".format(phone_prefix)
|
||||
|
||||
@@ -917,14 +902,8 @@ class BaseQuestionsForm(forms.Form):
|
||||
v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '')
|
||||
|
||||
def clean(self):
|
||||
from pretix.base.addressvalidation import \
|
||||
validate_address # local import to prevent impact on startup time
|
||||
|
||||
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 not d.get('state'):
|
||||
self.add_error('state', _('This field is required.'))
|
||||
@@ -1013,7 +992,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
|
||||
kwargs.setdefault('initial', {})
|
||||
if not kwargs.get('instance') or not kwargs['instance'].country:
|
||||
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
|
||||
kwargs['initial']['country'] = guess_country(self.event)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
if not event.settings.invoice_address_vatid:
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import re
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
@@ -30,7 +28,6 @@ from typing import Tuple
|
||||
|
||||
import bleach
|
||||
import vat_moss.exchange_rates
|
||||
from bidi.algorithm import get_display
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.db.models import Sum
|
||||
from django.dispatch import receiver
|
||||
@@ -56,8 +53,7 @@ from pretix.base.models import Event, Invoice, Order, OrderPayment
|
||||
from pretix.base.services.currencies import SOURCE_NAMES
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
|
||||
from pretix.presale.style import get_fonts
|
||||
from pretix.helpers.reportlab import ThumbnailingImageReader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -83,12 +79,7 @@ class NumberedCanvas(Canvas):
|
||||
def draw_page_number(self, page_count):
|
||||
self.saveState()
|
||||
self.setFont(self.font_regular, 8)
|
||||
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.drawRightString(self._pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d of %d") % (self._pageNumber, page_count,))
|
||||
self.restoreState()
|
||||
|
||||
|
||||
@@ -148,8 +139,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
"""
|
||||
Initialize the renderer. By default, this registers fonts and sets ``self.stylesheet``.
|
||||
"""
|
||||
self._register_fonts()
|
||||
self.stylesheet = self._get_stylesheet()
|
||||
self._register_fonts()
|
||||
|
||||
def _get_stylesheet(self):
|
||||
"""
|
||||
@@ -157,10 +148,6 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
"""
|
||||
stylesheet = StyleSheet1()
|
||||
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='BoldInverseCenter', fontName=self.font_bold, fontSize=10, leading=12,
|
||||
textColor=colors.white, alignment=TA_CENTER))
|
||||
@@ -168,7 +155,6 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
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='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
|
||||
|
||||
def _register_fonts(self):
|
||||
@@ -182,32 +168,6 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
|
||||
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):
|
||||
# We uppercase labels, but not in every language
|
||||
if get_language().startswith('el'):
|
||||
@@ -287,10 +247,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
return 'invoice.pdf', 'application/pdf', buffer.read()
|
||||
|
||||
def _clean_text(self, text, tags=None):
|
||||
return self._normalize(bleach.clean(
|
||||
return bleach.clean(
|
||||
text,
|
||||
tags=tags or []
|
||||
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
||||
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
|
||||
|
||||
|
||||
class PaidMarker(Flowable):
|
||||
@@ -331,7 +291,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
canvas.setFont(self.font_regular, 8)
|
||||
|
||||
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
|
||||
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, self._normalize(line.strip()))
|
||||
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
|
||||
|
||||
canvas.restoreState()
|
||||
|
||||
@@ -364,13 +324,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
def _draw_invoice_from_label(self, canvas):
|
||||
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice from'))))
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice from')))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
def _draw_invoice_to_label(self, canvas):
|
||||
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice to'))))
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice to')))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
logo_width = 25 * mm
|
||||
@@ -398,51 +358,51 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
def _draw_metadata(self, canvas):
|
||||
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Order code'))))
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Order code')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self._normalize(self.invoice.order.full_code))
|
||||
textobject.textLine(self.invoice.order.full_code)
|
||||
canvas.drawText(textobject)
|
||||
|
||||
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
if self.invoice.is_cancellation:
|
||||
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Cancellation number'))))
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation number')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self._normalize(self.invoice.number))
|
||||
textobject.textLine(self.invoice.number)
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Original invoice'))))
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self._normalize(self.invoice.refers.number))
|
||||
textobject.textLine(self.invoice.refers.number)
|
||||
else:
|
||||
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice number'))))
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice number')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self._normalize(self.invoice.number))
|
||||
textobject.textLine(self.invoice.number)
|
||||
textobject.moveCursor(0, 5)
|
||||
|
||||
if self.invoice.is_cancellation:
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Cancellation date'))))
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Cancellation date')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self._normalize(date_format(self.invoice.date, "DATE_FORMAT")))
|
||||
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Original invoice date'))))
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Original invoice date')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self._normalize(date_format(self.invoice.refers.date, "DATE_FORMAT")))
|
||||
textobject.textLine(date_format(self.invoice.refers.date, "DATE_FORMAT"))
|
||||
textobject.moveCursor(0, 5)
|
||||
else:
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice date'))))
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Invoice date')))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_regular, 10)
|
||||
textobject.textLine(self._normalize(date_format(self.invoice.date, "DATE_FORMAT")))
|
||||
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
|
||||
textobject.moveCursor(0, 5)
|
||||
|
||||
canvas.drawText(textobject)
|
||||
@@ -455,19 +415,19 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
def _draw_event_label(self, canvas):
|
||||
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
|
||||
textobject.setFont(self.font_bold, 8)
|
||||
textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Event'))))
|
||||
textobject.textLine(self._upper(pgettext('invoice', 'Event')))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
def _draw_event(self, canvas):
|
||||
def shorten(txt):
|
||||
txt = str(txt)
|
||||
txt = bleach.clean(txt, tags=[]).strip()
|
||||
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
|
||||
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
||||
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
||||
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
return txt
|
||||
|
||||
@@ -493,7 +453,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
else:
|
||||
p_str = shorten(self.invoice.event.name)
|
||||
|
||||
p = Paragraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, 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])
|
||||
@@ -502,14 +462,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
def _draw_footer(self, canvas):
|
||||
canvas.setFont(self.font_regular, 8)
|
||||
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]):
|
||||
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, self._normalize(line.strip()))
|
||||
canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
|
||||
|
||||
def _draw_testmode(self, canvas):
|
||||
if self.invoice.order.testmode:
|
||||
canvas.saveState()
|
||||
canvas.setFont(self.font_bold, 30)
|
||||
canvas.setFont('OpenSansBd', 30)
|
||||
canvas.setFillColorRGB(32, 0, 0)
|
||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, self._normalize(gettext('TEST MODE')))
|
||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE'))
|
||||
canvas.restoreState()
|
||||
|
||||
def _on_first_page(self, canvas: Canvas, doc):
|
||||
@@ -557,22 +517,22 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if self.invoice.internal_reference:
|
||||
story.append(Paragraph(
|
||||
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
|
||||
pgettext('invoice', 'Customer reference: {reference}').format(
|
||||
reference=self._clean_text(self.invoice.internal_reference),
|
||||
)),
|
||||
),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.invoice_to_vat_id:
|
||||
story.append(Paragraph(
|
||||
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
|
||||
pgettext('invoice', 'Customer VAT ID') + ': ' +
|
||||
self._clean_text(self.invoice.invoice_to_vat_id),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.invoice_to_beneficiary:
|
||||
story.append(Paragraph(
|
||||
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
|
||||
pgettext('invoice', 'Beneficiary') + ':<br />' +
|
||||
self._clean_text(self.invoice.invoice_to_beneficiary),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
@@ -592,10 +552,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
story = [
|
||||
NextPageTemplate('FirstPage'),
|
||||
Paragraph(
|
||||
self._normalize(
|
||||
(
|
||||
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
||||
else pgettext('invoice', 'Invoice')
|
||||
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')),
|
||||
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
|
||||
self.stylesheet['Heading1']
|
||||
),
|
||||
Spacer(1, 5 * mm),
|
||||
@@ -617,17 +577,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
]
|
||||
if has_taxes:
|
||||
tdata = [(
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
|
||||
pgettext('invoice', 'Description'),
|
||||
pgettext('invoice', 'Qty'),
|
||||
pgettext('invoice', 'Tax rate'),
|
||||
pgettext('invoice', 'Net'),
|
||||
pgettext('invoice', 'Gross'),
|
||||
)]
|
||||
else:
|
||||
tdata = [(
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['BoldRight']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
|
||||
pgettext('invoice', 'Description'),
|
||||
pgettext('invoice', 'Qty'),
|
||||
pgettext('invoice', 'Amount'),
|
||||
)]
|
||||
|
||||
def _group_key(line):
|
||||
@@ -674,13 +634,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if has_taxes:
|
||||
tdata.append([
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
|
||||
pgettext('invoice', 'Invoice total'), '', '', '',
|
||||
money_filter(total, self.invoice.event.currency)
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
|
||||
else:
|
||||
tdata.append([
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
|
||||
pgettext('invoice', 'Invoice total'), '',
|
||||
money_filter(total, self.invoice.event.currency)
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.65, .20, .15)]
|
||||
@@ -689,16 +649,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING:
|
||||
pending_sum = self.invoice.order.pending_sum
|
||||
if pending_sum != total:
|
||||
tdata.append(
|
||||
[Paragraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
|
||||
(['', '', ''] if has_taxes else ['']) +
|
||||
[money_filter(pending_sum - total, 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)]
|
||||
)
|
||||
tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [
|
||||
money_filter(pending_sum - total, self.invoice.event.currency)
|
||||
])
|
||||
tdata.append([pgettext('invoice', 'Outstanding payments')] + (['', '', ''] if has_taxes else ['']) + [
|
||||
money_filter(pending_sum, self.invoice.event.currency)
|
||||
])
|
||||
tstyledata += [
|
||||
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
|
||||
]
|
||||
@@ -711,24 +667,19 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
).aggregate(
|
||||
s=Sum('amount')
|
||||
)['s'] or Decimal('0.00')
|
||||
tdata.append(
|
||||
[Paragraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
|
||||
(['', '', ''] if has_taxes else ['']) +
|
||||
[money_filter(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)]
|
||||
)
|
||||
tdata.append([pgettext('invoice', 'Paid by gift card')] + (['', '', ''] if has_taxes else ['']) + [
|
||||
money_filter(giftcard_sum, self.invoice.event.currency)
|
||||
])
|
||||
tdata.append([pgettext('invoice', 'Remaining amount')] + (['', '', ''] if has_taxes else ['']) + [
|
||||
money_filter(total - giftcard_sum, self.invoice.event.currency)
|
||||
])
|
||||
tstyledata += [
|
||||
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
|
||||
]
|
||||
elif self.invoice.payment_provider_stamp:
|
||||
pm = PaidMarker(
|
||||
text=self._normalize(self.invoice.payment_provider_stamp),
|
||||
text=self.invoice.payment_provider_stamp,
|
||||
color=colors.HexColor(self.event.settings.theme_color_success),
|
||||
font=self.font_bold,
|
||||
size=16
|
||||
)
|
||||
tdata[-1][-2] = pm
|
||||
@@ -741,7 +692,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if self.invoice.payment_provider_text:
|
||||
story.append(Paragraph(
|
||||
self._normalize(self.invoice.payment_provider_text),
|
||||
self.invoice.payment_provider_text,
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
@@ -765,10 +716,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
||||
]
|
||||
thead = [
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
|
||||
pgettext('invoice', 'Tax rate'),
|
||||
pgettext('invoice', 'Net value'),
|
||||
pgettext('invoice', 'Gross value'),
|
||||
pgettext('invoice', 'Tax'),
|
||||
''
|
||||
]
|
||||
tdata = [thead]
|
||||
@@ -779,7 +730,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
continue
|
||||
tax = taxvalue_map[idx]
|
||||
tdata.append([
|
||||
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||
localize(rate) + " % " + name,
|
||||
money_filter(gross - tax, self.invoice.event.currency),
|
||||
money_filter(gross, self.invoice.event.currency),
|
||||
money_filter(tax, self.invoice.event.currency),
|
||||
@@ -798,7 +749,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
table.setStyle(TableStyle(tstyledata))
|
||||
story.append(Spacer(5 * mm, 5 * mm))
|
||||
story.append(KeepTogether([
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
|
||||
Paragraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
|
||||
table
|
||||
]))
|
||||
|
||||
@@ -815,7 +766,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
net = gross - tax
|
||||
|
||||
tdata.append([
|
||||
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||
localize(rate) + " % " + name,
|
||||
fmt(net), fmt(gross), fmt(tax), ''
|
||||
])
|
||||
|
||||
@@ -825,12 +776,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
story.append(KeepTogether([
|
||||
Spacer(1, height=2 * mm),
|
||||
Paragraph(
|
||||
self._normalize(pgettext(
|
||||
pgettext(
|
||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||
'{date}, this corresponds to:'
|
||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||
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']
|
||||
),
|
||||
Spacer(1, height=3 * mm),
|
||||
@@ -839,14 +790,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
||||
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
|
||||
story.append(Spacer(1, 5 * mm))
|
||||
story.append(Paragraph(self._normalize(
|
||||
story.append(Paragraph(
|
||||
pgettext(
|
||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||
'{date}, the invoice total corresponds to {total}.'
|
||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
|
||||
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
||||
total=fmt(foreign_total))),
|
||||
total=fmt(foreign_total)),
|
||||
self.stylesheet['Fineprint']
|
||||
))
|
||||
|
||||
@@ -892,7 +843,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
self._clean_text(l)
|
||||
for l in self.invoice.address_invoice_from.strip().split('\n')
|
||||
]
|
||||
p = Paragraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
|
||||
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
|
||||
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)
|
||||
super()._draw_invoice_from(canvas)
|
||||
@@ -908,12 +859,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
|
||||
def _get_first_page_frames(self, doc):
|
||||
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 [
|
||||
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - margin_top,
|
||||
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 95 * mm,
|
||||
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
|
||||
id='normal')
|
||||
]
|
||||
@@ -924,35 +871,25 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
# the font size until it fits.
|
||||
begin_top = 100 * mm
|
||||
|
||||
def _draw(label, value, value_size, x, width, bold=False, sublabel=None):
|
||||
def _draw(label, value, value_size, x, width):
|
||||
if canvas.stringWidth(value, self.font_regular, value_size) > width and value_size > 6:
|
||||
return False
|
||||
textobject = canvas.beginText(x, self.pagesize[1] - begin_top)
|
||||
textobject.setFont(self.font_regular, 8)
|
||||
textobject.textLine(self._normalize(label))
|
||||
textobject.textLine(label)
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont(self.font_bold if bold else self.font_regular, value_size)
|
||||
textobject.textLine(self._normalize(value))
|
||||
|
||||
if sublabel:
|
||||
textobject.moveCursor(0, 1)
|
||||
textobject.setFont(self.font_regular, 8)
|
||||
textobject.textLine(self._normalize(sublabel))
|
||||
|
||||
textobject.setFont(self.font_regular, value_size)
|
||||
textobject.textLine(value)
|
||||
return textobject
|
||||
|
||||
value_size = 10
|
||||
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 = [
|
||||
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
|
||||
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm)
|
||||
]
|
||||
|
||||
p = Paragraph(
|
||||
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
|
||||
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)
|
||||
)
|
||||
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
|
||||
@@ -983,9 +920,9 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top)
|
||||
textobject.setFont(self.font_regular, 8)
|
||||
if self.invoice.is_cancellation:
|
||||
textobject.textLine(self._normalize(pgettext('invoice', 'Cancellation date')))
|
||||
textobject.textLine(pgettext('invoice', 'Cancellation date'))
|
||||
else:
|
||||
textobject.textLine(self._normalize(pgettext('invoice', 'Invoice date')))
|
||||
textobject.textLine(pgettext('invoice', 'Invoice date'))
|
||||
canvas.drawText(textobject)
|
||||
|
||||
|
||||
|
||||
@@ -49,9 +49,8 @@ class Command(BaseCommand):
|
||||
except ImportError:
|
||||
cmd = 'shell'
|
||||
del options['skip_checks']
|
||||
del options['print_sql']
|
||||
|
||||
if options.get('print_sql'):
|
||||
if options['print_sql']:
|
||||
connection.force_debug_cursor = True
|
||||
logger = logging.getLogger("django.db.backends")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -39,7 +39,6 @@ from pretix.base.models.fields import MultiStringField
|
||||
from pretix.base.models.organizer import Organizer
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.helpers.countries import FastCountryField
|
||||
from pretix.helpers.names import build_name
|
||||
|
||||
|
||||
class CustomerSSOProvider(LoggedModel):
|
||||
@@ -172,11 +171,15 @@ class Customer(LoggedModel):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return build_name(self.name_parts, fallback_scheme=lambda: self.organizer.settings.name_scheme) or ""
|
||||
|
||||
@property
|
||||
def name_all_components(self):
|
||||
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.organizer.settings.name_scheme) or ""
|
||||
if not self.name_parts:
|
||||
return ""
|
||||
if '_legacy' in self.name_parts:
|
||||
return self.name_parts['_legacy']
|
||||
if '_scheme' in self.name_parts:
|
||||
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
|
||||
else:
|
||||
raise TypeError("Invalid name given.")
|
||||
return scheme['concatenation'](self.name_parts).strip()
|
||||
|
||||
def __str__(self):
|
||||
s = f'#{self.identifier}'
|
||||
@@ -299,11 +302,15 @@ class AttendeeProfile(models.Model):
|
||||
|
||||
@property
|
||||
def attendee_name(self):
|
||||
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
|
||||
|
||||
@property
|
||||
def attendee_name_all_components(self):
|
||||
return build_name(self.attendee_name_parts, "concatenation_all_components", fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
|
||||
if not self.attendee_name_parts:
|
||||
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()
|
||||
|
||||
@property
|
||||
def state_name(self):
|
||||
|
||||
@@ -290,19 +290,19 @@ class EventMixin:
|
||||
return safe_string(json.dumps(eventdict))
|
||||
|
||||
@classmethod
|
||||
def annotated(cls, qs, channel='web', voucher=None):
|
||||
def annotated(cls, qs, channel='web'):
|
||||
from pretix.base.models import Item, ItemVariation, Quota
|
||||
|
||||
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
|
||||
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel).filter(
|
||||
Q(variations__isnull=True)
|
||||
& Q(quotas__pk=OuterRef('pk'))
|
||||
).order_by().values_list('quotas__pk').annotate(
|
||||
items=GroupConcat('pk', delimiter=',')
|
||||
).values('items')
|
||||
|
||||
q_variation = (
|
||||
sq_active_variation = ItemVariation.objects.filter(
|
||||
Q(active=True)
|
||||
& Q(sales_channels__contains=channel)
|
||||
& Q(hide_without_voucher=False)
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
||||
& Q(item__active=True)
|
||||
@@ -310,23 +310,10 @@ class EventMixin:
|
||||
& 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(item__sales_channels__contains=channel)
|
||||
& Q(item__hide_without_voucher=False)
|
||||
& Q(item__require_bundling=False)
|
||||
& Q(quotas__pk=OuterRef('pk'))
|
||||
)
|
||||
|
||||
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(
|
||||
).order_by().values_list('quotas__pk').annotate(
|
||||
items=GroupConcat('pk', delimiter=',')
|
||||
).values('items')
|
||||
quota_base_qs = Quota.objects.using(settings.DATABASE_REPLICA).filter(
|
||||
@@ -638,7 +625,6 @@ class Event(EventMixin, LoggedModel):
|
||||
"""
|
||||
self.settings.invoice_renderer = 'modern1'
|
||||
self.settings.invoice_include_expire_date = True
|
||||
self.settings.invoice_renderer_highlight_order_code = True
|
||||
self.settings.ticketoutput_pdf__enabled = True
|
||||
self.settings.ticketoutput_passbook__enabled = True
|
||||
self.settings.event_list_type = 'calendar'
|
||||
@@ -1146,8 +1132,8 @@ class Event(EventMixin, LoggedModel):
|
||||
irs = self.get_invoice_renderers()
|
||||
return irs[self.settings.invoice_renderer]
|
||||
|
||||
def subevents_annotated(self, channel, voucher=None):
|
||||
return SubEvent.annotated(self.subevents, channel, voucher)
|
||||
def subevents_annotated(self, channel):
|
||||
return SubEvent.annotated(self.subevents, channel)
|
||||
|
||||
def subevents_sorted(self, queryset):
|
||||
ordering = self.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
|
||||
@@ -1467,10 +1453,10 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
return qs_annotated
|
||||
|
||||
@classmethod
|
||||
def annotated(cls, qs, channel='web', voucher=None):
|
||||
def annotated(cls, qs, channel='web'):
|
||||
from .items import SubEventItem, SubEventItemVariation
|
||||
|
||||
qs = super().annotated(qs, channel, voucher=voucher)
|
||||
qs = super().annotated(qs, channel)
|
||||
qs = qs.annotate(
|
||||
disabled_items=Coalesce(
|
||||
Subquery(
|
||||
|
||||
@@ -25,9 +25,7 @@ from django.conf import settings
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.html import format_html
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
@@ -68,13 +66,6 @@ class GiftCard(LoggedModel):
|
||||
on_delete=models.PROTECT,
|
||||
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(
|
||||
auto_now_add=True,
|
||||
)
|
||||
@@ -162,61 +153,6 @@ class GiftCardTransaction(models.Model):
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
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:
|
||||
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,15 +2001,6 @@ class ItemMetaProperty(LoggedModel):
|
||||
verbose_name=_("Name"),
|
||||
)
|
||||
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:
|
||||
ordering = ("name",)
|
||||
|
||||
@@ -31,7 +31,7 @@ from i18nfield.fields import I18nCharField
|
||||
from pretix.base.models import Customer
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.organizer import Organizer
|
||||
from pretix.helpers.names import build_name
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
|
||||
|
||||
class MembershipType(LoggedModel):
|
||||
@@ -160,7 +160,15 @@ class Membership(models.Model):
|
||||
|
||||
@property
|
||||
def attendee_name(self):
|
||||
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
|
||||
if not self.attendee_name_parts:
|
||||
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):
|
||||
if ev:
|
||||
|
||||
@@ -82,7 +82,6 @@ from pretix.base.signals import order_gracefully_delete
|
||||
from ...helpers import OF_SELF
|
||||
from ...helpers.countries import CachedCountries, FastCountryField
|
||||
from ...helpers.format import format_map
|
||||
from ...helpers.names import build_name
|
||||
from ._transactions import (
|
||||
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
|
||||
)
|
||||
@@ -831,7 +830,7 @@ class Order(LockModel, LoggedModel):
|
||||
@property
|
||||
def is_expired_by_time(self):
|
||||
return (
|
||||
self.status == Order.STATUS_PENDING and not self.require_approval and self.expires < now()
|
||||
self.status == Order.STATUS_PENDING and self.expires < now()
|
||||
and not self.event.settings.get('payment_term_expire_automatically')
|
||||
)
|
||||
|
||||
@@ -1213,7 +1212,7 @@ class QuestionAnswer(models.Model):
|
||||
|
||||
@property
|
||||
def is_image(self):
|
||||
return any(self.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE)
|
||||
return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.png', '.gif', '.tiff', '.bmp', '.jpeg'))
|
||||
|
||||
@property
|
||||
def file_name(self):
|
||||
@@ -1452,11 +1451,15 @@ class AbstractPosition(models.Model):
|
||||
|
||||
@property
|
||||
def attendee_name(self):
|
||||
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.event.settings.name_scheme)
|
||||
|
||||
@property
|
||||
def attendee_name_all_components(self):
|
||||
return build_name(self.attendee_name_parts, "concatenation_all_components", fallback_scheme=lambda: self.event.settings.name_scheme)
|
||||
if not self.attendee_name_parts:
|
||||
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.event.settings.name_scheme]
|
||||
return scheme['concatenation'](self.attendee_name_parts).strip()
|
||||
|
||||
@property
|
||||
def state_name(self):
|
||||
@@ -2831,12 +2834,8 @@ class CartPosition(AbstractPosition):
|
||||
if self.is_bundled:
|
||||
bundle = self.addon_to.item.bundles.filter(bundled_item=self.item, bundled_variation=self.variation).first()
|
||||
if bundle:
|
||||
if self.addon_to.voucher_id and self.addon_to.voucher.all_bundles_included:
|
||||
listed_price = Decimal('0.00')
|
||||
price_after_voucher = Decimal('0.00')
|
||||
else:
|
||||
listed_price = bundle.designated_price
|
||||
price_after_voucher = bundle.designated_price
|
||||
listed_price = bundle.designated_price
|
||||
price_after_voucher = bundle.designated_price
|
||||
|
||||
if listed_price != self.listed_price or price_after_voucher != self.price_after_voucher:
|
||||
self.listed_price = listed_price
|
||||
@@ -2981,11 +2980,15 @@ class InvoiceAddress(models.Model):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return build_name(self.name_parts, fallback_scheme=lambda: self.order.event.settings.name_scheme) or ""
|
||||
|
||||
@property
|
||||
def name_all_components(self):
|
||||
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.order.event.settings.name_scheme) or ""
|
||||
if not self.name_parts:
|
||||
return ""
|
||||
if '_legacy' in self.name_parts:
|
||||
return self.name_parts['_legacy']
|
||||
if '_scheme' in self.name_parts:
|
||||
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):
|
||||
d = {}
|
||||
|
||||
@@ -296,14 +296,6 @@ class Voucher(LoggedModel):
|
||||
verbose_name=_("Shows hidden products that match this voucher"),
|
||||
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')
|
||||
|
||||
@@ -462,7 +454,7 @@ class Voucher(LoggedModel):
|
||||
|
||||
@staticmethod
|
||||
def clean_voucher_code(data, event, pk):
|
||||
if 'code' in data and Voucher.objects.filter(Q(code__iexact=data['code'].upper()) & Q(event=event) & ~Q(pk=pk)).exists():
|
||||
if 'code' in data and Voucher.objects.filter(Q(code__iexact=data['code']) & Q(event=event) & ~Q(pk=pk)).exists():
|
||||
raise ValidationError(_('A voucher with this code already exists.'))
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -35,9 +35,9 @@ from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import User, Voucher
|
||||
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.names import build_name
|
||||
from .base import LoggedModel
|
||||
from .event import Event, SubEvent
|
||||
from .items import Item, ItemVariation
|
||||
@@ -119,7 +119,7 @@ class WaitingListEntry(LoggedModel):
|
||||
|
||||
def clean(self):
|
||||
try:
|
||||
WaitingListEntry.clean_duplicate(self.event, self.email, self.item, self.variation, self.subevent, self.pk)
|
||||
WaitingListEntry.clean_duplicate(self.email, self.item, self.variation, self.subevent, self.pk)
|
||||
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
|
||||
WaitingListEntry.clean_subevent(self.event, self.subevent)
|
||||
except ObjectDoesNotExist:
|
||||
@@ -136,11 +136,15 @@ class WaitingListEntry(LoggedModel):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return build_name(self.name_parts, fallback_scheme=lambda: self.event.settings.name_scheme)
|
||||
|
||||
@property
|
||||
def name_all_components(self):
|
||||
return build_name(self.name_parts, "concatenation_all_components", fallback_scheme=lambda: self.event.settings.name_scheme)
|
||||
if not self.name_parts:
|
||||
return None
|
||||
if '_legacy' in self.name_parts:
|
||||
return self.name_parts['_legacy']
|
||||
if '_scheme' in self.name_parts:
|
||||
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):
|
||||
availability = (
|
||||
@@ -215,19 +219,18 @@ class WaitingListEntry(LoggedModel):
|
||||
self.voucher = v
|
||||
self.save()
|
||||
|
||||
with language(self.locale, self.event.settings.region):
|
||||
self.send_mail(
|
||||
self.event.settings.mail_subject_waiting_list,
|
||||
self.event.settings.mail_text_waiting_list,
|
||||
get_email_context(
|
||||
event=self.event,
|
||||
waiting_list_entry=self,
|
||||
waiting_list_voucher=v,
|
||||
event_or_subevent=self.subevent or self.event,
|
||||
),
|
||||
user=user,
|
||||
auth=auth,
|
||||
)
|
||||
self.send_mail(
|
||||
self.event.settings.mail_subject_waiting_list,
|
||||
self.event.settings.mail_text_waiting_list,
|
||||
get_email_context(
|
||||
event=self.event,
|
||||
waiting_list_entry=self,
|
||||
waiting_list_voucher=v,
|
||||
event_or_subevent=self.subevent or self.event,
|
||||
),
|
||||
user=user,
|
||||
auth=auth,
|
||||
)
|
||||
|
||||
def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString],
|
||||
context: Dict[str, Any]=None, log_entry_type: str='pretix.waitinglist.email.sent',
|
||||
@@ -304,9 +307,9 @@ class WaitingListEntry(LoggedModel):
|
||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_duplicate(event, email, item, variation, subevent, pk):
|
||||
def clean_duplicate(email, item, variation, subevent, pk):
|
||||
if WaitingListEntry.objects.filter(
|
||||
item=item, variation=variation, email__iexact=email, voucher__isnull=True, subevent=subevent
|
||||
).exclude(pk=pk).count() >= event.settings.waiting_list_limit_per_user:
|
||||
).exclude(pk=pk).exists():
|
||||
raise ValidationError(_('You are already on this waiting list! We will notify '
|
||||
'you as soon as we have a ticket available for you.'))
|
||||
|
||||
@@ -1469,8 +1469,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
trans = gc.transactions.create(
|
||||
value=-1 * payment.amount,
|
||||
order=payment.order,
|
||||
payment=payment,
|
||||
acceptor=self.event.organizer,
|
||||
payment=payment
|
||||
)
|
||||
payment.info_data = {
|
||||
'gift_card': gc.pk,
|
||||
@@ -1491,8 +1490,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
trans = gc.transactions.create(
|
||||
value=refund.amount,
|
||||
order=refund.order,
|
||||
refund=refund,
|
||||
acceptor=self.event.organizer,
|
||||
refund=refund
|
||||
)
|
||||
refund.info_data = {
|
||||
'gift_card': gc.pk,
|
||||
|
||||
@@ -48,6 +48,7 @@ from functools import partial
|
||||
from io import BytesIO
|
||||
|
||||
import jsonschema
|
||||
from arabic_reshaper import ArabicReshaper
|
||||
from bidi.algorithm import get_display
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
@@ -56,6 +57,7 @@ from django.db.models import Max, Min
|
||||
from django.dispatch import receiver
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext
|
||||
@@ -76,12 +78,12 @@ from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import Paragraph
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.invoice import ThumbnailingImageReader
|
||||
from pretix.base.models import Order, OrderPosition, Question
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import layout_image_variables, layout_text_variables
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.phone_format import phone_format
|
||||
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -409,7 +411,7 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"label": _("Validity start date"),
|
||||
"editor_sample": _("2017-05-31"),
|
||||
"evaluate": lambda op, order, ev: date_format(
|
||||
op.valid_from.astimezone(timezone(ev.settings.timezone)),
|
||||
now().astimezone(timezone(ev.settings.timezone)),
|
||||
"SHORT_DATE_FORMAT"
|
||||
) if op.valid_from else ""
|
||||
}),
|
||||
@@ -521,7 +523,7 @@ def images_from_questions(sender, *args, **kwargs):
|
||||
else:
|
||||
a = op.answers.filter(question_id=question_id).first() or a
|
||||
|
||||
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE):
|
||||
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff")):
|
||||
return None
|
||||
else:
|
||||
if etag:
|
||||
@@ -697,6 +699,12 @@ def get_seat(op: OrderPosition):
|
||||
return None
|
||||
|
||||
|
||||
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
||||
'delete_harakat': True,
|
||||
'support_ligatures': False,
|
||||
}))
|
||||
|
||||
|
||||
class Renderer:
|
||||
|
||||
def __init__(self, event, layout, background_file):
|
||||
|
||||
@@ -24,11 +24,9 @@ import sys
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
import importlib_metadata as metadata
|
||||
from django.apps import AppConfig, apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from packaging.requirements import Requirement
|
||||
|
||||
|
||||
class PluginType(Enum):
|
||||
@@ -83,11 +81,12 @@ class PluginConfig(AppConfig, metaclass=PluginConfigMeta):
|
||||
raise ImproperlyConfigured("A pretix plugin config should have a PretixPluginMeta inner class.")
|
||||
|
||||
if hasattr(self.PretixPluginMeta, 'compatibility') and not os.environ.get("PRETIX_IGNORE_CONFLICTS") == "True":
|
||||
req = Requirement(self.PretixPluginMeta.compatibility)
|
||||
requirement_version = metadata.version(req.name)
|
||||
if not req.specifier.contains(requirement_version, prereleases=True):
|
||||
import pkg_resources
|
||||
try:
|
||||
pkg_resources.require(self.PretixPluginMeta.compatibility)
|
||||
except pkg_resources.VersionConflict as e:
|
||||
print("Incompatible plugins found!")
|
||||
print("Plugin {} requires you to have {}, but you installed {}.".format(
|
||||
self.name, req, requirement_version
|
||||
self.name, e.req, e.dist
|
||||
))
|
||||
sys.exit(1)
|
||||
|
||||
@@ -512,10 +512,7 @@ class CartManager:
|
||||
if cp.is_bundled:
|
||||
bundle = cp.addon_to.item.bundles.filter(bundled_item=cp.item, bundled_variation=cp.variation).first()
|
||||
if bundle:
|
||||
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
|
||||
listed_price = bundle.designated_price or Decimal('0.00')
|
||||
else:
|
||||
listed_price = cp.price
|
||||
price_after_voucher = listed_price
|
||||
@@ -715,11 +712,6 @@ class CartManager:
|
||||
else:
|
||||
bundle_quotas = []
|
||||
|
||||
if voucher and voucher.all_bundles_included:
|
||||
bundled_price = Decimal('0.00')
|
||||
else:
|
||||
bundled_price = bundle.designated_price
|
||||
|
||||
bop = self.AddOperation(
|
||||
count=bundle.count,
|
||||
item=bitem,
|
||||
@@ -730,8 +722,8 @@ class CartManager:
|
||||
subevent=subevent,
|
||||
bundled=[],
|
||||
seat=None,
|
||||
listed_price=bundled_price,
|
||||
price_after_voucher=bundled_price,
|
||||
listed_price=bundle.designated_price,
|
||||
price_after_voucher=bundle.designated_price,
|
||||
custom_price_input=None,
|
||||
custom_price_input_is_net=False,
|
||||
voucher_ignored=False,
|
||||
@@ -817,6 +809,7 @@ class CartManager:
|
||||
quota_diff = Counter() # Quota -> Number of usages
|
||||
operations = []
|
||||
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(
|
||||
addon_to__isnull=True
|
||||
).prefetch_related(
|
||||
@@ -826,6 +819,7 @@ class CartManager:
|
||||
# Prefill some of the cache containers
|
||||
for cp in toplevel_cp:
|
||||
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
|
||||
for a in cp.addons.all():
|
||||
if not a.is_bundled:
|
||||
|
||||
@@ -740,11 +740,11 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
else:
|
||||
raise CheckInError(
|
||||
_('This ticket is only valid after {datetime}.').format(
|
||||
datetime=date_format(op.valid_from.astimezone(clist.event.timezone), 'SHORT_DATETIME_FORMAT')
|
||||
datetime=date_format(op.valid_from, 'SHORT_DATETIME_FORMAT')
|
||||
),
|
||||
'invalid_time',
|
||||
_('This ticket is only valid after {datetime}.').format(
|
||||
datetime=date_format(op.valid_from.astimezone(clist.event.timezone), 'SHORT_DATETIME_FORMAT')
|
||||
datetime=date_format(op.valid_from, 'SHORT_DATETIME_FORMAT')
|
||||
),
|
||||
)
|
||||
|
||||
@@ -754,11 +754,11 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
else:
|
||||
raise CheckInError(
|
||||
_('This ticket was only valid before {datetime}.').format(
|
||||
datetime=date_format(op.valid_until.astimezone(clist.event.timezone), 'SHORT_DATETIME_FORMAT')
|
||||
datetime=date_format(op.valid_until, 'SHORT_DATETIME_FORMAT')
|
||||
),
|
||||
'invalid_time',
|
||||
_('This ticket was only valid before {datetime}.').format(
|
||||
datetime=date_format(op.valid_until.astimezone(clist.event.timezone), 'SHORT_DATETIME_FORMAT')
|
||||
datetime=date_format(op.valid_until, 'SHORT_DATETIME_FORMAT')
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -95,18 +95,6 @@ class SendMailException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def clean_sender_name(sender_name: str) -> str:
|
||||
# Emails with @ in their sender name are rejected by some mailservers (e.g. Microsoft) because it looks like
|
||||
# a phishing attempt.
|
||||
sender_name = sender_name.replace("@", " ")
|
||||
|
||||
# Emails with excessively long sender names are rejected by some mailservers
|
||||
if len(sender_name) > 75:
|
||||
sender_name = sender_name[:75] + "..."
|
||||
|
||||
return sender_name
|
||||
|
||||
|
||||
def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString],
|
||||
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
|
||||
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
|
||||
@@ -208,13 +196,17 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
settings.MAIL_FROM
|
||||
)
|
||||
if event:
|
||||
sender_name = clean_sender_name(event.settings.mail_from_name or str(event.name))
|
||||
sender_name = event.settings.mail_from_name or str(event.name)
|
||||
if len(sender_name) > 75:
|
||||
sender_name = sender_name[:75] + "..."
|
||||
sender = formataddr((sender_name, sender))
|
||||
elif organizer:
|
||||
sender_name = clean_sender_name(organizer.settings.mail_from_name or str(organizer.name))
|
||||
sender_name = organizer.settings.mail_from_name or str(organizer.name)
|
||||
if len(sender_name) > 75:
|
||||
sender_name = sender_name[:75] + "..."
|
||||
sender = formataddr((sender_name, sender))
|
||||
else:
|
||||
sender = formataddr((clean_sender_name(settings.PRETIX_INSTANCE_NAME), sender))
|
||||
sender = formataddr((settings.PRETIX_INSTANCE_NAME, sender))
|
||||
|
||||
subject = raw_subject = str(subject).replace('\n', ' ').replace('\r', '')[:900]
|
||||
signature = ""
|
||||
@@ -466,17 +458,9 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
for inv in invoices:
|
||||
if inv.file:
|
||||
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):
|
||||
email.attach(
|
||||
filename,
|
||||
pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf',
|
||||
inv.file.file.read(),
|
||||
'application/pdf'
|
||||
)
|
||||
|
||||
@@ -95,8 +95,7 @@ from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
|
||||
from pretix.base.signals import (
|
||||
allow_ticket_download, order_approved, order_canceled, order_changed,
|
||||
order_denied, order_expired, order_fee_calculation, order_paid,
|
||||
order_placed, order_split, order_valid_if_pending, periodic_task,
|
||||
validate_order,
|
||||
order_placed, order_split, periodic_task, validate_order,
|
||||
)
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers import OF_SELF
|
||||
@@ -236,7 +235,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
|
||||
|
||||
for gc in position.issued_gift_cards.all():
|
||||
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
|
||||
gc.transactions.create(value=position.price, order=order, acceptor=order.event.organizer)
|
||||
gc.transactions.create(value=position.price, order=order)
|
||||
break
|
||||
|
||||
for m in position.granted_memberships.all():
|
||||
@@ -392,15 +391,9 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
|
||||
if order.total == Decimal('0.00'):
|
||||
email_template = order.event.settings.mail_text_order_approved_free
|
||||
email_subject = order.event.settings.mail_subject_order_approved_free
|
||||
email_attendees = order.event.settings.mail_send_order_approved_free_attendee
|
||||
email_attendee_template = order.event.settings.mail_text_order_approved_free_attendee
|
||||
email_attendee_subject = order.event.settings.mail_subject_order_approved_free_attendee
|
||||
else:
|
||||
email_template = order.event.settings.mail_text_order_approved
|
||||
email_subject = order.event.settings.mail_subject_order_approved
|
||||
email_attendees = order.event.settings.mail_send_order_approved_attendee
|
||||
email_attendee_template = order.event.settings.mail_text_order_approved_attendee
|
||||
email_attendee_subject = order.event.settings.mail_subject_order_approved_attendee
|
||||
|
||||
email_context = get_email_context(event=order.event, order=order)
|
||||
try:
|
||||
@@ -413,19 +406,6 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
|
||||
except SendMailException:
|
||||
logger.exception('Order approved email could not be sent')
|
||||
|
||||
if email_attendees:
|
||||
for p in order.positions.all():
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||
email_attendee_context = get_email_context(event=order.event, order=order, position=p)
|
||||
try:
|
||||
p.send_mail(
|
||||
email_attendee_subject, email_attendee_template, email_attendee_context,
|
||||
'pretix.event.order.email.order_approved', user,
|
||||
attach_tickets=True,
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order approved email could not be sent to attendee')
|
||||
|
||||
return order.pk
|
||||
|
||||
|
||||
@@ -514,7 +494,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
)
|
||||
)
|
||||
else:
|
||||
gc.transactions.create(value=-position.price, order=order, acceptor=order.event.organizer)
|
||||
gc.transactions.create(value=-position.price, order=order)
|
||||
|
||||
for m in position.granted_memberships.all():
|
||||
m.canceled = True
|
||||
@@ -923,7 +903,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,
|
||||
payment_requests: List[dict], locale: str=None, address: InvoiceAddress=None,
|
||||
meta_info: dict=None, sales_channel: str='web', shown_total=None,
|
||||
customer=None, valid_if_pending=False):
|
||||
customer=None):
|
||||
payments = []
|
||||
sales_channel = get_all_sales_channels()[sales_channel]
|
||||
|
||||
@@ -951,7 +931,6 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
require_approval=require_approval,
|
||||
sales_channel=sales_channel.identifier,
|
||||
customer=customer,
|
||||
valid_if_pending=valid_if_pending,
|
||||
)
|
||||
if customer:
|
||||
order.email_known_to_work = customer.is_verified
|
||||
@@ -1096,20 +1075,6 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
||||
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
|
||||
locked = False
|
||||
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2)) | Q(seat__isnull=False)).exists():
|
||||
@@ -1133,7 +1098,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)
|
||||
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests,
|
||||
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
|
||||
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
|
||||
shown_total=shown_total, customer=customer)
|
||||
try:
|
||||
for p in payment_objs:
|
||||
if p.provider == 'free':
|
||||
@@ -1458,16 +1423,6 @@ class OrderChangeManager:
|
||||
'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.'),
|
||||
'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'))
|
||||
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
|
||||
@@ -1477,7 +1432,7 @@ class OrderChangeManager:
|
||||
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
|
||||
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
|
||||
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership',
|
||||
'valid_from', 'valid_until', 'is_bundled'))
|
||||
'valid_from', 'valid_until'))
|
||||
SplitOperation = namedtuple('SplitOperation', ('position',))
|
||||
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
|
||||
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
|
||||
@@ -1707,7 +1662,6 @@ class OrderChangeManager:
|
||||
except TaxRule.SaleNotAllowed:
|
||||
raise OrderError(self.error_messages['tax_rule_country_blocked'])
|
||||
|
||||
is_bundled = False
|
||||
if price is None:
|
||||
raise OrderError(self.error_messages['product_invalid'])
|
||||
if item.variations.exists() and not variation:
|
||||
@@ -1716,10 +1670,7 @@ class OrderChangeManager:
|
||||
raise OrderError(self.error_messages['addon_to_required'])
|
||||
if addon_to:
|
||||
if not item.category or item.category_id not in addon_to.item.addons.values_list('addon_category', flat=True):
|
||||
if addon_to.item.bundles.filter(bundled_item=item, bundled_variation=variation).exists():
|
||||
is_bundled = True
|
||||
else:
|
||||
raise OrderError(self.error_messages['addon_invalid'])
|
||||
raise OrderError(self.error_messages['addon_invalid'])
|
||||
if self.order.event.has_subevents and not subevent:
|
||||
raise OrderError(self.error_messages['subevent_required'])
|
||||
|
||||
@@ -1744,7 +1695,7 @@ class OrderChangeManager:
|
||||
if seat:
|
||||
self._seatdiff.update([seat])
|
||||
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership,
|
||||
valid_from, valid_until, is_bundled))
|
||||
valid_from, valid_until))
|
||||
|
||||
def split(self, position: OrderPosition):
|
||||
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
|
||||
@@ -1770,11 +1721,6 @@ class OrderChangeManager:
|
||||
if self._operations:
|
||||
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
|
||||
current_addons = defaultdict(lambda: defaultdict(list)) # OrderPos -> currently attached add-ons
|
||||
input_addons = defaultdict(Counter) # OrderPos -> final desired set of add-ons
|
||||
@@ -1892,7 +1838,7 @@ class OrderChangeManager:
|
||||
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)
|
||||
|
||||
if price_included[op.pk].get(item.category_id) or (op.voucher_id and op.voucher.all_addons_included):
|
||||
if price_included[op.pk].get(item.category_id):
|
||||
price = TAXED_ZERO
|
||||
else:
|
||||
price = get_price(
|
||||
@@ -1911,7 +1857,6 @@ class OrderChangeManager:
|
||||
item=item, variation=variation, price=price,
|
||||
addon_to=op, subevent=op.subevent, seat=None,
|
||||
)
|
||||
item_counts[item] += 1
|
||||
|
||||
# Check constraints on the add-on combinations
|
||||
for op in toplevel_op:
|
||||
@@ -1961,27 +1906,6 @@ class OrderChangeManager:
|
||||
}
|
||||
)
|
||||
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):
|
||||
for seat, diff in self._seatdiff.items():
|
||||
@@ -2239,7 +2163,7 @@ class OrderChangeManager:
|
||||
card=gc.secret
|
||||
))
|
||||
else:
|
||||
gc.transactions.create(value=-op.position.price, order=self.order, acceptor=self.order.event.organizer)
|
||||
gc.transactions.create(value=-op.position.price, order=self.order)
|
||||
|
||||
for m in op.position.granted_memberships.with_usages().all():
|
||||
m.canceled = True
|
||||
@@ -2255,7 +2179,7 @@ class OrderChangeManager:
|
||||
card=gc.secret
|
||||
))
|
||||
else:
|
||||
gc.transactions.create(value=-opa.position.price, order=self.order, acceptor=self.order.event.organizer)
|
||||
gc.transactions.create(value=-opa.position.price, order=self.order)
|
||||
|
||||
for m in opa.granted_memberships.with_usages().all():
|
||||
m.canceled = True
|
||||
@@ -2302,7 +2226,6 @@ class OrderChangeManager:
|
||||
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
|
||||
positionid=nextposid, subevent=op.subevent, seat=op.seat,
|
||||
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
|
||||
is_bundled=op.is_bundled,
|
||||
)
|
||||
nextposid += 1
|
||||
self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
|
||||
@@ -2971,7 +2894,7 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
|
||||
currency=sender.currency, issued_in=p, testmode=order.testmode,
|
||||
expires=sender.organizer.default_gift_card_expiry,
|
||||
)
|
||||
gc.transactions.create(value=p.price - issued, order=order, acceptor=sender.organizer)
|
||||
gc.transactions.create(value=p.price - issued, order=order)
|
||||
any_giftcards = True
|
||||
p.secret = gc.secret
|
||||
p.save(update_fields=['secret'])
|
||||
|
||||
@@ -110,8 +110,6 @@ def is_included_for_free(item: Item, addon_to: AbstractPosition):
|
||||
return True
|
||||
except ItemAddOn.DoesNotExist:
|
||||
pass
|
||||
if addon_to.voucher_id and addon_to.voucher.all_addons_included:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
||||
for ir, r in enumerate(recipients):
|
||||
voucher_list = []
|
||||
for i in range(r['number']):
|
||||
voucher_list.append(vouchers.pop(0))
|
||||
voucher_list.append(vouchers.pop())
|
||||
with language(event.settings.locale):
|
||||
email_context = get_email_context(event=event, name=r.get('name') or '',
|
||||
voucher_list=[v.code for v in voucher_list])
|
||||
|
||||
@@ -96,18 +96,6 @@ 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():
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
@@ -606,7 +594,6 @@ DEFAULTS = {
|
||||
'form_kwargs': dict(
|
||||
label=_("Minimum length of invoice number after prefix"),
|
||||
help_text=_("The part of your invoice number after your prefix will be filled up with leading zeros up to this length, e.g. INV-001 or INV-00001."),
|
||||
max_value=12,
|
||||
required=True,
|
||||
)
|
||||
},
|
||||
@@ -633,17 +620,6 @@ DEFAULTS = {
|
||||
"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 "
|
||||
"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': {
|
||||
@@ -655,42 +631,8 @@ DEFAULTS = {
|
||||
label=_("Invoice number prefix for cancellations"),
|
||||
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."),
|
||||
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': {
|
||||
'default': 'classic', # default for new events is 'modern1'
|
||||
'type': str,
|
||||
@@ -1387,21 +1329,6 @@ DEFAULTS = {
|
||||
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': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -1444,10 +1371,9 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_class': forms.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Generate tickets for add-on products and bundled products"),
|
||||
help_text=_('By default, tickets are only issued for products selected individually, not for add-on products '
|
||||
'or bundled products. With this option, a separate ticket is issued for every add-on product '
|
||||
'or bundled product as well.'),
|
||||
label=_("Generate tickets for add-on products"),
|
||||
help_text=_('By default, tickets are only issued for products selected individually, not for add-on '
|
||||
'products. With this option, a separate ticket is issued for every add-on product as well.'),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_ticket_download',
|
||||
'data-checkbox-dependency-visual': 'on'}),
|
||||
)
|
||||
@@ -2347,26 +2273,6 @@ You can select a payment method and perform the payment here:
|
||||
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
},
|
||||
'mail_send_order_approved_attendee': {
|
||||
'type': bool,
|
||||
'default': 'False'
|
||||
},
|
||||
'mail_subject_order_approved_attendee': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("Your event registration: {code}")),
|
||||
},
|
||||
'mail_text_order_approved_attendee': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
we approved a ticket ordered for you for {event}.
|
||||
|
||||
You can view the details and status of your ticket here:
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
},
|
||||
@@ -2384,26 +2290,6 @@ at our event. As you only ordered free products, no payment is required.
|
||||
You can change your order details and view the status of your order at
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
},
|
||||
'mail_send_order_approved_free_attendee': {
|
||||
'type': bool,
|
||||
'default': 'False'
|
||||
},
|
||||
'mail_subject_order_approved_free_attendee': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("Your event registration: {code}")),
|
||||
},
|
||||
'mail_text_order_approved_free_attendee': {
|
||||
'type': LazyI18nString,
|
||||
'default': LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
we approved a ticket ordered for you for {event}.
|
||||
|
||||
You can view the details and status of your ticket here:
|
||||
{url}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
},
|
||||
@@ -2665,15 +2551,6 @@ Your {organizer} team"""))
|
||||
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': {
|
||||
'default': 'Open Sans',
|
||||
'type': str,
|
||||
@@ -2710,7 +2587,7 @@ Your {organizer} team"""))
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Header image'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||
help_text=_('If you provide a logo image, we will by default not show your event name and date '
|
||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||
@@ -2753,7 +2630,7 @@ Your {organizer} team"""))
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Header image'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||
@@ -2793,7 +2670,7 @@ Your {organizer} team"""))
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Social media image'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
|
||||
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
|
||||
@@ -2814,7 +2691,7 @@ Your {organizer} team"""))
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
|
||||
@@ -3401,7 +3278,6 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
||||
('full_name', _('Full name'), 2),
|
||||
),
|
||||
'concatenation': lambda d: str(d.get('full_name', '')),
|
||||
'concatenation_all_components': lambda d: str(d.get('full_name', '')) + " (\"" + d.get('calling_name', '') + "\")",
|
||||
'sample': {
|
||||
'full_name': pgettext_lazy('person_name_sample', 'John Doe'),
|
||||
'calling_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
@@ -3414,7 +3290,6 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
||||
('latin_transcription', _('Latin transcription'), 2),
|
||||
),
|
||||
'concatenation': lambda d: str(d.get('full_name', '')),
|
||||
'concatenation_all_components': lambda d: str(d.get('full_name', '')) + " (" + d.get('latin_transcription', '') + ")",
|
||||
'sample': {
|
||||
'full_name': '庄司',
|
||||
'latin_transcription': 'Shōji',
|
||||
@@ -3431,9 +3306,6 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
||||
str(p) for p in (d.get(key, '') for key in ["given_name", "family_name"]) if p
|
||||
),
|
||||
'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': {
|
||||
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
|
||||
'given_name': pgettext_lazy('person_name_sample', 'John'),
|
||||
@@ -3452,9 +3324,6 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
||||
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_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': {
|
||||
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
|
||||
'title': pgettext_lazy('person_name_sample', 'Dr'),
|
||||
@@ -3479,13 +3348,6 @@ PERSON_NAME_SCHEMES = OrderedDict([
|
||||
str(d.get('degree', ''))
|
||||
),
|
||||
'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': {
|
||||
'salutation': pgettext_lazy('person_name_sample', 'Mr'),
|
||||
'title': pgettext_lazy('person_name_sample', 'Dr'),
|
||||
|
||||
@@ -304,7 +304,8 @@ multiple events. Receivers should return a subclass of pretix.base.exporter.Base
|
||||
The ``sender`` keyword argument will contain an organizer.
|
||||
"""
|
||||
|
||||
validate_order = EventPluginSignal()
|
||||
validate_order = EventPluginSignal(
|
||||
)
|
||||
"""
|
||||
Arguments: ``payments``, ``positions``, ``email``, ``locale``, ``invoice_address``,
|
||||
``meta_info``, ``customer``
|
||||
@@ -320,18 +321,6 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
|
||||
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()
|
||||
"""
|
||||
Arguments: ``positions``
|
||||
@@ -589,20 +578,6 @@ All plugins that are installed may send fields for the global settings form, as
|
||||
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()
|
||||
"""
|
||||
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" %}" />
|
||||
{% endcompress %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/reloadpending.js" %}"></script>
|
||||
{% endcompress %}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
@@ -71,7 +71,6 @@ class BaseQuestionsViewMixin:
|
||||
kwargs = self.question_form_kwargs(cr)
|
||||
form = self.form_class(event=self.request.event,
|
||||
prefix=cr.id,
|
||||
request=self.request,
|
||||
cartpos=cartpos,
|
||||
orderpos=orderpos,
|
||||
all_optional=self.all_optional,
|
||||
|
||||
@@ -127,7 +127,7 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
|
||||
|
||||
@property
|
||||
def is_img(self):
|
||||
return any(self.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_IMAGE)
|
||||
return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.jpeg', '.png', '.gif'))
|
||||
|
||||
def __str__(self):
|
||||
if hasattr(self.file, 'display_name'):
|
||||
|
||||
@@ -542,7 +542,6 @@ class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
'waiting_list_phones_asked',
|
||||
'waiting_list_phones_required',
|
||||
'waiting_list_phones_explanation_text',
|
||||
'waiting_list_limit_per_user',
|
||||
'max_items_per_order',
|
||||
'reservation_time',
|
||||
'contact_mail',
|
||||
@@ -856,8 +855,6 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
'invoice_footer_text',
|
||||
'invoice_eu_currencies',
|
||||
'invoice_logo_image',
|
||||
'invoice_renderer_highlight_order_code',
|
||||
'invoice_renderer_font',
|
||||
]
|
||||
|
||||
invoice_generate_sales_channels = forms.MultipleChoiceField(
|
||||
@@ -1194,24 +1191,6 @@ class MailSettingsForm(SettingsForm):
|
||||
help_text=_("This will only be sent out for non-free orders. Free orders will receive the free order "
|
||||
"template from below instead."),
|
||||
)
|
||||
mail_send_order_approved_attendee = forms.BooleanField(
|
||||
label=_("Send an email to attendees"),
|
||||
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
|
||||
'tickets, the following email will be sent out to the attendees.'),
|
||||
required=False,
|
||||
)
|
||||
mail_subject_order_approved_attendee = I18nFormField(
|
||||
label=_("Subject sent to attendees"),
|
||||
required=False,
|
||||
widget=I18nTextInput,
|
||||
)
|
||||
mail_text_order_approved_attendee = I18nFormField(
|
||||
label=_("Text sent to attendees"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("This will only be sent out for non-free orders. Free orders will receive the free order "
|
||||
"template from below instead."),
|
||||
)
|
||||
mail_subject_order_approved_free = I18nFormField(
|
||||
label=_("Subject for approved free order"),
|
||||
required=False,
|
||||
@@ -1224,24 +1203,6 @@ class MailSettingsForm(SettingsForm):
|
||||
help_text=_("This will only be sent out for free orders. Non-free orders will receive the non-free order "
|
||||
"template from above instead."),
|
||||
)
|
||||
mail_send_order_approved_free_attendee = forms.BooleanField(
|
||||
label=_("Send an email to attendees"),
|
||||
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
|
||||
'tickets, the following email will be sent out to the attendees.'),
|
||||
required=False,
|
||||
)
|
||||
mail_subject_order_approved_free_attendee = I18nFormField(
|
||||
label=_("Subject sent to attendees"),
|
||||
required=False,
|
||||
widget=I18nTextInput,
|
||||
)
|
||||
mail_text_order_approved_free_attendee = I18nFormField(
|
||||
label=_("Text sent to attendees"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("This will only be sent out for free orders. Non-free orders will receive the non-free order "
|
||||
"template from above instead."),
|
||||
)
|
||||
mail_subject_order_denied = I18nFormField(
|
||||
label=_("Subject for denied order"),
|
||||
required=False,
|
||||
@@ -1473,7 +1434,6 @@ class TaxRuleForm(I18nModelForm):
|
||||
class WidgetCodeForm(forms.Form):
|
||||
subevent = forms.ModelChoiceField(
|
||||
label=pgettext_lazy('subevent', "Date"),
|
||||
empty_label=pgettext_lazy('subevent', "All dates"),
|
||||
required=False,
|
||||
queryset=SubEvent.objects.none()
|
||||
)
|
||||
@@ -1674,7 +1634,7 @@ QuickSetupProductFormSet = formset_factory(
|
||||
|
||||
class ItemMetaPropertyForm(forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ['name', 'default', 'required', 'allowed_values']
|
||||
fields = ['name', 'default']
|
||||
widgets = {
|
||||
'default': forms.TextInput()
|
||||
}
|
||||
|
||||
@@ -256,13 +256,9 @@ class OrderFilterForm(FilterForm):
|
||||
else:
|
||||
code = Q(code__icontains=Order.normalize_code(u))
|
||||
|
||||
invoice_nos = {u, u.upper()}
|
||||
if u.isdigit():
|
||||
for i in range(2, 12):
|
||||
invoice_nos.add(u.zfill(i))
|
||||
|
||||
matching_invoices = Invoice.objects.filter(
|
||||
Q(invoice_no__in=invoice_nos)
|
||||
Q(invoice_no__iexact=u)
|
||||
| Q(invoice_no__iexact=u.zfill(5))
|
||||
| Q(full_invoice_no__iexact=u)
|
||||
).values_list('order_id', flat=True)
|
||||
matching_positions = OrderPosition.objects.filter(
|
||||
@@ -483,7 +479,7 @@ class EventOrderFilterForm(OrderFilterForm):
|
||||
file__isnull=False
|
||||
)
|
||||
qs = qs.annotate(has_answer=Exists(answers)).filter(has_answer=True)
|
||||
elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE) and fdata.get('answer'):
|
||||
elif q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||
answers = QuestionAnswer.objects.filter(
|
||||
question_id=q.pk,
|
||||
orderposition__order_id=OuterRef('pk'),
|
||||
@@ -1008,13 +1004,9 @@ class OrderPaymentSearchFilterForm(forms.Form):
|
||||
if fdata.get('query'):
|
||||
u = fdata.get('query')
|
||||
|
||||
invoice_nos = {u, u.upper()}
|
||||
if u.isdigit():
|
||||
for i in range(2, 12):
|
||||
invoice_nos.add(u.zfill(i))
|
||||
|
||||
matching_invoices = Invoice.objects.filter(
|
||||
Q(invoice_no__in=invoice_nos)
|
||||
Q(invoice_no__iexact=u)
|
||||
| Q(invoice_no__iexact=u.zfill(5))
|
||||
| Q(full_invoice_no__iexact=u)
|
||||
).values_list('order_id', flat=True)
|
||||
|
||||
@@ -1338,7 +1330,6 @@ class GiftCardFilterForm(FilterForm):
|
||||
Q(secret__icontains=query)
|
||||
| Q(transactions__text__icontains=query)
|
||||
| Q(transactions__order__code__icontains=query)
|
||||
| Q(owner_ticket__order__code__icontains=query)
|
||||
)
|
||||
if fdata.get('testmode') == 'yes':
|
||||
qs = qs.filter(testmode=True)
|
||||
|
||||
@@ -1102,26 +1102,16 @@ class ItemMetaValueForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.property = kwargs.pop('property')
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.property.allowed_values:
|
||||
self.fields['value'] = forms.ChoiceField(
|
||||
label=self.property.name,
|
||||
choices=[(
|
||||
"", _("Default ({value})").format(value=self.property.default)
|
||||
if self.property.default else ""
|
||||
)] + [(a.strip(), a.strip()) for a in self.property.allowed_values.splitlines()]
|
||||
)
|
||||
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
|
||||
self.fields['value'].required = False
|
||||
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,
|
||||
})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemMetaValue
|
||||
|
||||
@@ -186,15 +186,6 @@ class OrganizerUpdateForm(OrganizerForm):
|
||||
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 Meta:
|
||||
model = EventMetaProperty
|
||||
@@ -416,7 +407,7 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
|
||||
organizer_logo_image = ExtFileField(
|
||||
label=_('Header image'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||
required=False,
|
||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||
@@ -426,7 +417,7 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
)
|
||||
favicon = ExtFileField(
|
||||
label=_('Favicon'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_FAVICON,
|
||||
ext_whitelist=(".ico", ".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
|
||||
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
|
||||
@@ -659,32 +650,23 @@ class GiftCardCreateForm(forms.ModelForm):
|
||||
class GiftCardUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ['expires', 'conditions', 'owner_ticket']
|
||||
fields = ['expires', 'conditions']
|
||||
field_classes = {
|
||||
'expires': SplitDateTimeField,
|
||||
'owner_ticket': SafeOrderPositionChoiceField,
|
||||
'expires': SplitDateTimeField
|
||||
}
|
||||
widgets = {
|
||||
'expires': SplitDateTimePickerWidget,
|
||||
'conditions': forms.Textarea(attrs={"rows": 2})
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
organizer = self.instance.issuer
|
||||
|
||||
self.fields['owner_ticket'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
|
||||
self.fields['owner_ticket'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
|
||||
'organizer': organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('Ticket')
|
||||
}
|
||||
)
|
||||
self.fields['owner_ticket'].widget.choices = self.fields['owner_ticket'].choices
|
||||
self.fields['owner_ticket'].required = False
|
||||
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 ReusableMediumUpdateForm(forms.ModelForm):
|
||||
|
||||
@@ -72,8 +72,7 @@ class VoucherForm(I18nModelForm):
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
|
||||
'comment', 'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included',
|
||||
'all_bundles_included', 'budget'
|
||||
'comment', 'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
|
||||
]
|
||||
field_classes = {
|
||||
'valid_until': SplitDateTimeField,
|
||||
@@ -309,8 +308,7 @@ class VoucherBulkForm(VoucherForm):
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
|
||||
'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included',
|
||||
'all_bundles_included', 'budget'
|
||||
'max_usages', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
|
||||
]
|
||||
field_classes = {
|
||||
'valid_until': SplitDateTimeField,
|
||||
|
||||
@@ -485,9 +485,6 @@ 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.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_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.deleted': _('The quota has been deleted.'),
|
||||
'pretix.event.quota.changed': _('The quota has been changed.'),
|
||||
@@ -509,7 +506,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),
|
||||
'pretix.event.checkinlist.added': _('The check-in list has been added.'),
|
||||
'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.settings': _('The event settings have been changed.'),
|
||||
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
|
||||
|
||||
@@ -80,7 +80,7 @@ class PermissionMiddleware:
|
||||
"user.settings.2fa.disable",
|
||||
"user.settings.2fa.regenemergency",
|
||||
"user.settings.2fa.confirm.totp",
|
||||
"user.settings.2fa.confirm.webauthn",
|
||||
"user.settings.2fa.confirm.u2f",
|
||||
"user.settings.2fa.delete",
|
||||
"auth.logout",
|
||||
"user.reauth"
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</script>
|
||||
{% endif %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.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>
|
||||
{% endcompress %}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<script src="{% statici18n request.LANGUAGE_CODE %}"></script>
|
||||
{% endif %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.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 "bootstrap/js/bootstrap.js" %}"></script>
|
||||
|
||||
@@ -13,25 +13,20 @@
|
||||
{% elif payment_info.payment_type == "terminal_zvt" %}
|
||||
<dt>{% trans "Payment provider" %}</dt>
|
||||
<dd>{% trans "ZVT Terminal" %}</dd>
|
||||
{% if payment_info.payment_data.source == "manual" %}
|
||||
<dt>{% trans "Confirmation mode" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.source }}</dd>
|
||||
{% else %}
|
||||
<dt>{% trans "Trace number" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.traceNumber }}</dd>
|
||||
<dt>{% trans "Payment type" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.paymentType }}</dd>
|
||||
<dt>{% trans "Additional text" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.additionalText }}</dd>
|
||||
<dt>{% trans "Turnover number" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.turnoverNumber }}</dd>
|
||||
<dt>{% trans "Receipt number" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.receiptNumber }}</dd>
|
||||
<dt>{% trans "Card type" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.cardName|default_if_none:payment_info.payment_data.cardType }}</dd>
|
||||
<dt>{% trans "Card expiration" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.expiry }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Trace number" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.traceNumber }}</dd>
|
||||
<dt>{% trans "Payment type" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.paymentType }}</dd>
|
||||
<dt>{% trans "Additional text" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.additionalText }}</dd>
|
||||
<dt>{% trans "Turnover number" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.turnoverNumber }}</dd>
|
||||
<dt>{% trans "Receipt number" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.receiptNumber }}</dd>
|
||||
<dt>{% trans "Card type" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.cardName|default_if_none:payment_info.payment_data.cardType }}</dd>
|
||||
<dt>{% trans "Card expiration" context "terminal_zvt" %}</dt>
|
||||
<dd>{{ payment_info.payment_data.expiry }}</dd>
|
||||
{% elif payment_info.payment_type == "sumup" %}
|
||||
<dt>{% trans "Payment provider" %}</dt>
|
||||
<dd>SumUp</dd>
|
||||
|
||||
@@ -54,8 +54,6 @@
|
||||
{% bootstrap_field form.invoice_additional_text layout="control" %}
|
||||
{% bootstrap_field form.invoice_footer_text layout="control" %}
|
||||
{% bootstrap_field form.invoice_logo_image layout="control" %}
|
||||
{% bootstrap_field form.invoice_renderer_font layout="control" %}
|
||||
{% bootstrap_field form.invoice_renderer_highlight_order_code layout="control" %}
|
||||
{% bootstrap_field form.invoice_eu_currencies layout="control" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_subject_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_subject_download_reminder_attendee,mail_text_download_reminder_attendee,mail_sales_channel_download_reminder" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee,mail_sales_channel_download_reminder" %}
|
||||
|
||||
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
|
||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_subject_order_placed_require_approval,mail_text_order_placed_require_approval,mail_subject_order_approved,mail_text_order_approved,mail_send_order_approved_attendee,mail_subject_order_approved_attendee,mail_text_order_approved_attendee,mail_subject_order_approved_free,mail_text_order_approved_free,mail_send_order_approved_free_attendee,mail_subject_order_approved_free_attendee,mail_text_order_approved_free_attendee,mail_subject_order_denied,mail_text_order_denied" exclude="mail_send_order_approved_attendee,mail_send_order_approved_free_attendee"%}
|
||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_subject_order_placed_require_approval,mail_text_order_placed_require_approval,mail_subject_order_approved,mail_text_order_approved,mail_subject_order_approved_free,mail_text_order_approved_free,mail_subject_order_denied,mail_text_order_denied" %}
|
||||
</div>
|
||||
<h4>{% trans "Attachments" %}</h4>
|
||||
{% bootstrap_field form.mail_attachment_new_order layout="control" %}
|
||||
|
||||
@@ -361,7 +361,6 @@
|
||||
{% 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_explanation_text layout="control" %}
|
||||
{% bootstrap_field sform.waiting_list_limit_per_user layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Item metadata" %}</legend>
|
||||
@@ -378,55 +377,41 @@
|
||||
{% bootstrap_formset_errors item_meta_property_formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in item_meta_property_formset %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="row formset-row" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="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">
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.default layout="control" %}
|
||||
{% bootstrap_field form.required layout="control" %}
|
||||
{% bootstrap_field form.allowed_values layout="control" %}
|
||||
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-5 col-lg-6">
|
||||
{% bootstrap_field form.default layout='inline' form_group_class="" %}
|
||||
</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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="row formset-row" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ item_meta_property_formset.empty_form.id }}
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<h3 class="panel-title">{% trans "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 class="col-md-5">
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.name layout="control" %}
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.default layout="control" %}
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.required layout="control" %}
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.allowed_values layout="control" %}
|
||||
<div class="col-md-5 col-lg-6">
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.default layout='inline' form_group_class="" %}
|
||||
</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>
|
||||
{% endescapescript %}
|
||||
|
||||
@@ -139,26 +139,6 @@
|
||||
</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>
|
||||
{% if form.quota_option %}
|
||||
<fieldset>
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{% bootstrap_form form layout="inline" error_types="all" %}
|
||||
{% bootstrap_form form layout="inline" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<noscript>
|
||||
<p>{% trans "Only applicable if you choose 'Choose one/multiple from a list' above." %}</p>
|
||||
</noscript>
|
||||
<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." %}">
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
|
||||
@@ -364,7 +364,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% for line in items.positions %}
|
||||
<div class="row-fluid product-row {% if line.canceled %}pos-canceled{% endif %} {% if line.item.require_approval and order.require_approval and order.status == 'n' %}bg-warning{% endif %}">
|
||||
<div class="row-fluid product-row {% if line.canceled %}pos-canceled{% endif %}">
|
||||
<div class="col-md-9 col-xs-6">
|
||||
{% if line.addon_to %}
|
||||
<span class="addon-signifier">+</span>
|
||||
@@ -511,7 +511,7 @@
|
||||
<dl>
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_names_asked %}
|
||||
<dt>{% trans "Attendee name" %}</dt>
|
||||
<dd>{% if line.attendee_name %}{{ line.attendee_name_all_components }}{% else %}
|
||||
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}
|
||||
<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
||||
{% endif %}
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_emails_asked %}
|
||||
@@ -631,12 +631,6 @@
|
||||
</div>
|
||||
<div class="clearfix"></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 %}
|
||||
{% for fee in items.fees %}
|
||||
<div class="row-fluid product-row {% if fee.canceled %}pos-canceled{% endif %}">
|
||||
|
||||
@@ -204,21 +204,13 @@
|
||||
{% endblocktrans %}
|
||||
</th>
|
||||
<th class="text-right flip">
|
||||
{% if not filter_form.filtered %}
|
||||
<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 %}
|
||||
{% if sums.s|default_if_none:"none" != "none" %}
|
||||
{{ sums.s|money:request.event.currency }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<th class="text-right flip">
|
||||
{% if not filter_form.filtered %}
|
||||
<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 %}
|
||||
{% if sums.pc %}
|
||||
{{ sums.pc }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<th></th>
|
||||
|
||||
@@ -45,14 +45,6 @@
|
||||
{% bootstrap_field filter_form.date_until layout='inline' %}
|
||||
</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">
|
||||
<button class="btn btn-primary btn-lg" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<script type="text/json" data-replace-with-qr>{{ qrdata|safe }}</script><br>
|
||||
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
|
||||
<br>
|
||||
<strong>{% trans "System URL:" %}</strong> <code>{{ settings.SITE_URL }}</code><br>
|
||||
<strong>{% trans "Token:" %}</strong> <code>{{ device.initialization_token }}</code>
|
||||
<strong>{% trans "System URL:" %}</strong> {{ settings.SITE_URL }}<br>
|
||||
<strong>{% trans "Token:" %}</strong> {{ device.initialization_token }}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -97,13 +97,7 @@
|
||||
<div class="list-group large-link-group">
|
||||
{% for e in c_ex %}
|
||||
<a class="list-group-item" href="?identifier={{ e.identifier }}">
|
||||
<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>
|
||||
<h4>{{ e.verbose_name }}</h4>
|
||||
{% if e.description %}
|
||||
<p>
|
||||
{{ e.description }}
|
||||
|
||||
@@ -46,14 +46,6 @@
|
||||
{{ card.issued_in.order.full_code }}</a>-{{ card.issued_in.positionid }}
|
||||
</dd>
|
||||
{% 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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,32 +62,29 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "Information" %}</th>
|
||||
<th>{% trans "Order" %}</th>
|
||||
<th class="text-right">{% trans "Value" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in transactions %}
|
||||
{% for t in card.transactions.all %}
|
||||
<tr>
|
||||
<td>{{ t.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>
|
||||
{{ t.display_backend }}
|
||||
{% if t.refund and t.value > 0 and t.value <= card.value %}
|
||||
<button type="submit" name="revert" value="{{ t.pk }}"
|
||||
class="btn btn-default btn-xs" data-toggle="tooltip"
|
||||
title="{% trans "Create a payment on the respective order that cancels out with this transaction. The order will then likely be overpaid." %}">
|
||||
<span class="fa fa-repeat"></span>
|
||||
{% trans "Revert" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if staff_session and t.info %}
|
||||
<pre><code>{{ t.info|pprint }}</code></pre>
|
||||
{% endif %}
|
||||
{% if t.acceptor and t.acceptor != request.organizer %}
|
||||
<span class="text-muted">
|
||||
<br>
|
||||
<span class="fa fa-group"></span> {{ t.acceptor }}
|
||||
</span>
|
||||
{% if t.order %}
|
||||
<a href="{% url "control:event.order" event=t.order.event.slug organizer=t.order.event.organizer.slug code=t.order.code %}">
|
||||
{{ t.order.full_code }}
|
||||
</a>
|
||||
{% if t.refund and t.value > 0 and t.value <= card.value %}
|
||||
<button type="submit" name="revert" value="{{ t.pk }}"
|
||||
class="btn btn-default btn-xs" data-toggle="tooltip"
|
||||
title="{% trans "Create a payment on the respective order that cancels out with this transaction. The order will then likely be overpaid." %}">
|
||||
<span class="fa fa-repeat"></span>
|
||||
{% trans "Revert" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "Manual transaction" %}{% if t.text %}: {{ t.text }}{% endif %}</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.expires layout="control" %}
|
||||
{% bootstrap_field form.owner_ticket layout="control" %}
|
||||
{% bootstrap_field form.conditions layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user