forked from CGM_Public/pretix_original
Compare commits
275 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 118c206bb1 | |||
| a56c6ae1e0 | |||
| 1efd952a19 | |||
| ddd0db3d98 | |||
| dde724d0be | |||
| 9f55187690 | |||
| e572bfb752 | |||
| beccdf8dad | |||
| b141ea4ed5 | |||
| 32dd125b65 | |||
| b5794780da | |||
| 835c08f1ca | |||
| a79d5fddda | |||
| beb03e07e0 | |||
| dd2e5d09f9 | |||
| 5c2456e92e | |||
| 3579a7f298 | |||
| 5dd20de745 | |||
| 5708099fc9 | |||
| 2c2e8e7d21 | |||
| 5993482f6c | |||
| c905659dfb | |||
| 196c131ac9 | |||
| a554433fad | |||
| c552dd876c | |||
| 105ae8592d | |||
| ca4540eeb7 | |||
| 381366a248 | |||
| 816a3ec994 | |||
| 88d3d12dbc | |||
| 71caa17879 | |||
| 2ecdfde756 | |||
| 1f753a57c5 | |||
| 595c042624 | |||
| 5a5a551c21 | |||
| e74793994a | |||
| f1bdd3b7af | |||
| 13c40f9bb7 | |||
| 3e15e2a887 | |||
| 7525ee853b | |||
| f02b1be659 | |||
| 20f171b790 | |||
| 22906dfa77 | |||
| 5f74e661b3 | |||
| 5b99788354 | |||
| b45d58b60e | |||
| 2a58e958b0 | |||
| e44695dfcf | |||
| 1d289088f4 | |||
| a9be6337bc | |||
| df29d4e8c4 | |||
| bf2cabf7b6 | |||
| db614d36e6 | |||
| 13452b5d8c | |||
| 6422cd7858 | |||
| 2e87cb5691 | |||
| b53ee938bf | |||
| ec3bdd4a57 | |||
| 7b607594d8 | |||
| 53f129d5d3 | |||
| a4385c8b6e | |||
| 3acae96021 | |||
| b9add5ff6f | |||
| 4ca9813a1d | |||
| 347748896d | |||
| 0f590caa18 | |||
| 18801f2d1c | |||
| e5f29bd592 | |||
| 1f904d482b | |||
| b8ad276f53 | |||
| e109c37738 | |||
| 4d597d5be3 | |||
| ae8ec42905 | |||
| e5b89e9b08 | |||
| da91f5f117 | |||
| ae29240e58 | |||
| 74edf10b04 | |||
| e2e0eca872 | |||
| 6132e4a2c4 | |||
| 7df7d28518 | |||
| 11ab5c5eeb | |||
| 20211d2097 | |||
| d760ad38bf | |||
| 69af2cee93 | |||
| 6b199a2b9c | |||
| 94a64ba53a | |||
| 70f06a8f40 | |||
| a747ab154a | |||
| 6317233150 | |||
| 4d94158ff0 | |||
| 8f92eb2d2d | |||
| f29896b267 | |||
| 2dc625cf31 | |||
| 855226d37c | |||
| 648c0da9fe | |||
| 59e3494fa2 | |||
| c4ff57c07a | |||
| cc4fbfe4c7 | |||
| e99ee91573 | |||
| e2753686ee | |||
| 33f8b9851e | |||
| e3d8cf07af | |||
| 0279ca7d94 | |||
| d1989c3cd3 | |||
| 61cb2e15cf | |||
| f2ee1d00b3 | |||
| e8e9698a31 | |||
| a1bf7be244 | |||
| f4ca9a5681 | |||
| e6d984538f | |||
| 9f1ee9157f | |||
| 242e5af4b5 | |||
| 7d6e98e6da | |||
| 27f964f3ae | |||
| 84b3060c0f | |||
| 25dcb72f92 | |||
| 4b078867c6 | |||
| c595a59d4a | |||
| f164daeaee | |||
| c6b6dd8d49 | |||
| 8038c87963 | |||
| c45a970d32 | |||
| a34517233d | |||
| 8fb2e5383c | |||
| 86a00f3338 | |||
| c8c0d3e7f5 | |||
| 7dd455ce15 | |||
| 391eda25da | |||
| fcff5a522d | |||
| 7e93d38a01 | |||
| 6469381899 | |||
| 761706c60c | |||
| f91315c88e | |||
| bc05afeab9 | |||
| 02d495d287 | |||
| 894878d9da | |||
| 5896ca0197 | |||
| fe6fc8df32 | |||
| 9de8f3a775 | |||
| c92bb9cb8b | |||
| 76ecec8b98 | |||
| 4b8416df8f | |||
| a601c75923 | |||
| f94227f00f | |||
| a0c1e5369c | |||
| 633bfcf73a | |||
| 0d3b5b82c1 | |||
| ab95f33546 | |||
| 5034b366c5 | |||
| 03d3c389da | |||
| 3e934acfa0 | |||
| d2a364e848 | |||
| 2824b40299 | |||
| c6c2c90908 | |||
| d4ae7df2ec | |||
| 79dd7fb596 | |||
| 5ed87cd019 | |||
| ccdcbe0cc5 | |||
| 4f8607a9db | |||
| 57ecaa2676 | |||
| 96fd2b1a95 | |||
| 5cf24fb6a6 | |||
| 1d2ea35a39 | |||
| ac98ae7941 | |||
| a0d055e202 | |||
| 27ec5ca006 | |||
| 9d2edc405d | |||
| fb95fe7cf6 | |||
| 5b5360ef8b | |||
| 129d10ca35 | |||
| 093a705ff9 | |||
| 6130ae4630 | |||
| 11a8ed6c7a | |||
| f6392592c5 | |||
| ecb9ad28ea | |||
| 45a506fd37 | |||
| 3b16e6356b | |||
| 9583a50c4e | |||
| 6e6d6b2746 | |||
| 7266d90c6b | |||
| 5e4e88c91d | |||
| e74d12e8b8 | |||
| a5c39271dd | |||
| 3170744c56 | |||
| 9ec161561b | |||
| aff4f4b8f8 | |||
| 75addfe9f4 | |||
| 4b05ce5835 | |||
| 34c247f423 | |||
| 3aad6852cb | |||
| 5cdb07bce6 | |||
| 6cb2d68948 | |||
| 4a7a6273c6 | |||
| ebe343458a | |||
| f9a93b765c | |||
| 5aba1f9a23 | |||
| a4eed87396 | |||
| 08879d0d55 | |||
| c276a19bcc | |||
| 1e3c6e0b68 | |||
| 4e283eb560 | |||
| 52a1983630 | |||
| 3d85d9d865 | |||
| 4ca9a43890 | |||
| d8bac7db65 | |||
| 91de0f93e6 | |||
| 901565203b | |||
| 14c6c9c0d7 | |||
| 6de6cf6c08 | |||
| 29306b3a4d | |||
| ca69996611 | |||
| 16419b6ae4 | |||
| d6258b9b54 | |||
| 6f75608196 | |||
| 6ef88e009b | |||
| 957100a195 | |||
| 112ef0908f | |||
| 91aaff7359 | |||
| 8ab61e2c38 | |||
| c8ba5cc427 | |||
| 5ebad31b7d | |||
| 0429377f7d | |||
| 76e4b797a1 | |||
| 5f0009c996 | |||
| de63a4be01 | |||
| f3432139cb | |||
| 0b82ac9115 | |||
| eb685b5141 | |||
| 5f7f0bd8f1 | |||
| 9fcef2dcaa | |||
| fc3b186b93 | |||
| a406884575 | |||
| 57ccd5f289 | |||
| f4ac7e7f65 | |||
| 81d7045b31 | |||
| f9502a3212 | |||
| a31f624417 | |||
| 3f99e0bece | |||
| 7e64f2b38a | |||
| ee2bc93608 | |||
| fb4bed9d0d | |||
| aec75e4d0c | |||
| e7e41470fb | |||
| 0aa9dda90a | |||
| d97c983b6f | |||
| 6c957f31ca | |||
| 8e6b4b3ec7 | |||
| b24de62f73 | |||
| cdbd220a12 | |||
| 2f11aee512 | |||
| 8ea475ce39 | |||
| b29bc9db96 | |||
| 6bd6694132 | |||
| 110e6e248e | |||
| 985f4d969d | |||
| 826bd07b01 | |||
| 3e4e86742a | |||
| ef5fcde5d9 | |||
| 8f1d53d016 | |||
| 9ca1573fcf | |||
| 5795aa6492 | |||
| 6e0613a2af | |||
| b43ed38483 | |||
| f0fedf0001 | |||
| 19373b8f91 | |||
| 45fd13786a | |||
| ae5111ee7e | |||
| d8bf3065d0 | |||
| 54f077665c | |||
| 482a66c546 | |||
| e4cef6e46b | |||
| cbee1b71fe | |||
| 0cd1290624 | |||
| 565f5e2ea7 | |||
| b46c0eba0c |
@@ -38,7 +38,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt update && sudo apt install gettext unzip
|
run: sudo apt update && sudo apt install -y gettext unzip
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: pip3 install -U setuptools build pip check-manifest
|
run: pip3 install -U setuptools build pip check-manifest
|
||||||
- name: Run check-manifest
|
- name: Run check-manifest
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
run: sudo apt update && sudo apt install enchant-2 hunspell aspell-en
|
run: sudo apt update && sudo apt install -y enchant-2 hunspell aspell-en
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -Ur requirements.txt
|
run: pip3 install -Ur requirements.txt
|
||||||
working-directory: ./doc
|
working-directory: ./doc
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
run: sudo apt update && sudo apt install gettext
|
run: sudo apt update && sudo apt -y install gettext
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -e ".[dev]"
|
run: pip3 install uv && uv pip install --system -e ".[dev]"
|
||||||
- name: Compile messages
|
- name: Compile messages
|
||||||
run: python manage.py compilemessages
|
run: python manage.py compilemessages
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de
|
run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -e ".[dev]"
|
run: pip3 install uv && uv pip install --system -e ".[dev]"
|
||||||
- name: Spellcheck translations
|
- name: Spellcheck translations
|
||||||
run: potypo
|
run: potypo
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -e ".[dev]" psycopg2-binary
|
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||||
- name: Run isort
|
- name: Run isort
|
||||||
run: isort -c .
|
run: isort -c .
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -e ".[dev]" psycopg2-binary
|
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||||
- name: Run flake8
|
- name: Run flake8
|
||||||
run: flake8 .
|
run: flake8 .
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
|
|||||||
+19
-13
@@ -30,15 +30,21 @@ jobs:
|
|||||||
python-version: "3.9"
|
python-version: "3.9"
|
||||||
- database: sqlite
|
- database: sqlite
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: pretix
|
||||||
|
options: >-
|
||||||
|
--health-cmd "pg_isready -U postgres -d pretix"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: harmon758/postgresql-action@v1
|
|
||||||
with:
|
|
||||||
postgresql version: '15'
|
|
||||||
postgresql db: 'pretix'
|
|
||||||
postgresql user: 'postgres'
|
|
||||||
postgresql password: 'postgres'
|
|
||||||
if: matrix.database == 'postgres'
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -50,9 +56,9 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt update && sudo apt install gettext
|
run: sudo apt update && sudo apt install -y gettext
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: pip3 install --ignore-requires-python -e ".[dev]" psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
|
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||||
- name: Run checks
|
- name: Run checks
|
||||||
run: python manage.py check
|
run: python manage.py check
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
@@ -64,15 +70,15 @@ jobs:
|
|||||||
run: make all compress
|
run: make all compress
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
|
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml tests --maxfail=100
|
||||||
- name: Run concurrency tests
|
- name: Run concurrency tests
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
|
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reuse-db
|
||||||
if: matrix.database == 'postgres'
|
if: matrix.database == 'postgres'
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
file: src/coverage.xml
|
file: src/coverage.xml
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: false
|
||||||
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ tests:
|
|||||||
- cd src
|
- cd src
|
||||||
- python manage.py check
|
- python manage.py check
|
||||||
- make all compress
|
- make all compress
|
||||||
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
|
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
|
||||||
except:
|
except:
|
||||||
- pypi
|
- pypi
|
||||||
pypi:
|
pypi:
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ http {
|
|||||||
deny all;
|
deny all;
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
location /static/staticfiles.json {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
location /static/CACHE/manifest.json {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
location /static/ {
|
location /static/ {
|
||||||
alias /pretix/src/pretix/static.dist/;
|
alias /pretix/src/pretix/static.dist/;
|
||||||
access_log off;
|
access_log off;
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ Example::
|
|||||||
[django]
|
[django]
|
||||||
secret=j1kjps5a5&4ilpn912s7a1!e2h!duz^i3&idu@_907s$wrz@x-
|
secret=j1kjps5a5&4ilpn912s7a1!e2h!duz^i3&idu@_907s$wrz@x-
|
||||||
debug=off
|
debug=off
|
||||||
|
passwords_argon2=on
|
||||||
|
|
||||||
``secret``
|
``secret``
|
||||||
The secret to be used by Django for signing and verification purposes. If this
|
The secret to be used by Django for signing and verification purposes. If this
|
||||||
@@ -303,6 +304,10 @@ Example::
|
|||||||
|
|
||||||
.. WARNING:: Never set this to ``True`` in production!
|
.. WARNING:: Never set this to ``True`` in production!
|
||||||
|
|
||||||
|
``passwords_argon``
|
||||||
|
Use the ``argon2`` algorithm for password hashing. Disable on systems with a small number of CPU cores (currently
|
||||||
|
less than 8).
|
||||||
|
|
||||||
``profile``
|
``profile``
|
||||||
Enable code profiling for a random subset of requests. Disabled by default, see
|
Enable code profiling for a random subset of requests. Disabled by default, see
|
||||||
:ref:`perf-monitoring` for details.
|
:ref:`perf-monitoring` for details.
|
||||||
|
|||||||
@@ -231,11 +231,10 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
listen 443 default_server;
|
listen 443 ssl default_server;
|
||||||
listen [::]:443 ipv6only=on default_server;
|
listen [::]:443 ipv6only=on ssl default_server;
|
||||||
server_name pretix.mydomain.com;
|
server_name pretix.mydomain.com;
|
||||||
|
|
||||||
ssl on;
|
|
||||||
ssl_certificate /path/to/cert.chain.pem;
|
ssl_certificate /path/to/cert.chain.pem;
|
||||||
ssl_certificate_key /path/to/key.pem;
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
|||||||
@@ -216,11 +216,10 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
listen 443 default_server;
|
listen 443 ssl default_server;
|
||||||
listen [::]:443 ipv6only=on default_server;
|
listen [::]:443 ipv6only=on ssl default_server;
|
||||||
server_name pretix.mydomain.com;
|
server_name pretix.mydomain.com;
|
||||||
|
|
||||||
ssl on;
|
|
||||||
ssl_certificate /path/to/cert.chain.pem;
|
ssl_certificate /path/to/cert.chain.pem;
|
||||||
ssl_certificate_key /path/to/key.pem;
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
@@ -249,6 +248,14 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
|||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /static/staticfiles.json {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
location /static/CACHE/manifest.json {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
location /static/ {
|
location /static/ {
|
||||||
alias /var/pretix/venv/lib/python3.11/site-packages/pretix/static.dist/;
|
alias /var/pretix/venv/lib/python3.11/site-packages/pretix/static.dist/;
|
||||||
access_log off;
|
access_log off;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ Endpoints
|
|||||||
"mode": "placed",
|
"mode": "placed",
|
||||||
"all_sales_channels": false,
|
"all_sales_channels": false,
|
||||||
"limit_sales_channels": ["web"],
|
"limit_sales_channels": ["web"],
|
||||||
"all_products": False,
|
"all_products": false,
|
||||||
"limit_products": [2, 3],
|
"limit_products": [2, 3],
|
||||||
"limit_variations": [456],
|
"limit_variations": [456],
|
||||||
"all_payment_methods": true,
|
"all_payment_methods": true,
|
||||||
@@ -113,7 +113,7 @@ Endpoints
|
|||||||
"mode": "placed",
|
"mode": "placed",
|
||||||
"all_sales_channels": false,
|
"all_sales_channels": false,
|
||||||
"limit_sales_channels": ["web"],
|
"limit_sales_channels": ["web"],
|
||||||
"all_products": False,
|
"all_products": false,
|
||||||
"limit_products": [2, 3],
|
"limit_products": [2, 3],
|
||||||
"limit_variations": [456],
|
"limit_variations": [456],
|
||||||
"all_payment_methods": true,
|
"all_payment_methods": true,
|
||||||
@@ -146,7 +146,7 @@ Endpoints
|
|||||||
"mode": "placed",
|
"mode": "placed",
|
||||||
"all_sales_channels": false,
|
"all_sales_channels": false,
|
||||||
"limit_sales_channels": ["web"],
|
"limit_sales_channels": ["web"],
|
||||||
"all_products": False,
|
"all_products": false,
|
||||||
"limit_products": [2, 3],
|
"limit_products": [2, 3],
|
||||||
"limit_variations": [456],
|
"limit_variations": [456],
|
||||||
"all_payment_methods": true,
|
"all_payment_methods": true,
|
||||||
@@ -167,7 +167,7 @@ Endpoints
|
|||||||
"mode": "placed",
|
"mode": "placed",
|
||||||
"all_sales_channels": false,
|
"all_sales_channels": false,
|
||||||
"limit_sales_channels": ["web"],
|
"limit_sales_channels": ["web"],
|
||||||
"all_products": False,
|
"all_products": false,
|
||||||
"limit_products": [2, 3],
|
"limit_products": [2, 3],
|
||||||
"limit_variations": [456],
|
"limit_variations": [456],
|
||||||
"all_payment_methods": true,
|
"all_payment_methods": true,
|
||||||
@@ -216,7 +216,7 @@ Endpoints
|
|||||||
"mode": "placed",
|
"mode": "placed",
|
||||||
"all_sales_channels": false,
|
"all_sales_channels": false,
|
||||||
"limit_sales_channels": ["web"],
|
"limit_sales_channels": ["web"],
|
||||||
"all_products": False,
|
"all_products": false,
|
||||||
"limit_products": [2, 3],
|
"limit_products": [2, 3],
|
||||||
"limit_variations": [456],
|
"limit_variations": [456],
|
||||||
"all_payment_methods": true,
|
"all_payment_methods": true,
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ subevent integer ID of the date
|
|||||||
position_count integer Number of tickets that match this list (read-only).
|
position_count integer Number of tickets that match this list (read-only).
|
||||||
checkin_count integer Number of check-ins performed on this list (read-only).
|
checkin_count integer Number of check-ins performed on this list (read-only).
|
||||||
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
||||||
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
|
|
||||||
**Deprecated, will be removed in pretix 2024.10.** Use :ref:`rest-autocheckinrules`: instead.
|
|
||||||
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
||||||
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
||||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||||
@@ -91,10 +89,7 @@ Endpoints
|
|||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"exit_all_at": null,
|
"exit_all_at": null,
|
||||||
"rules": {},
|
"rules": {},
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -146,10 +141,7 @@ Endpoints
|
|||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"exit_all_at": null,
|
"exit_all_at": null,
|
||||||
"rules": {},
|
"rules": {},
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -246,10 +238,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"allow_multiple_entries": false,
|
"allow_multiple_entries": false,
|
||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -271,10 +260,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"allow_multiple_entries": false,
|
"allow_multiple_entries": false,
|
||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
|
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
|
||||||
@@ -326,10 +312,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"allow_multiple_entries": false,
|
"allow_multiple_entries": false,
|
||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
@@ -342,7 +325,7 @@ Endpoints
|
|||||||
|
|
||||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
|
||||||
|
|
||||||
Delete a check-in list. Note that this also deletes the information on all check-ins performed via this list.
|
Delete a check-in list. **Note that this also deletes the information on all check-ins performed via this list.**
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ lines list of objects The actual invo
|
|||||||
├ gross_value money (string) Price including taxes
|
├ gross_value money (string) Price including taxes
|
||||||
├ tax_value money (string) Tax amount included
|
├ tax_value money (string) Tax amount included
|
||||||
├ tax_name string Name of used tax rate (e.g. "VAT")
|
├ tax_name string Name of used tax rate (e.g. "VAT")
|
||||||
|
├ tax_code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||||
└ tax_rate decimal (string) Used tax rate
|
└ tax_rate decimal (string) Used tax rate
|
||||||
foreign_currency_display string If the invoice should also show the total and tax
|
foreign_currency_display string If the invoice should also show the total and tax
|
||||||
amount in a different currency, this contains the
|
amount in a different currency, this contains the
|
||||||
@@ -126,6 +127,10 @@ internal_reference string Customer's refe
|
|||||||
|
|
||||||
The ``event`` attribute has been added. The organizer-level endpoint has been added.
|
The ``event`` attribute has been added. The organizer-level endpoint has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2024.8
|
||||||
|
|
||||||
|
The ``tax_code`` attribute has been added.
|
||||||
|
|
||||||
|
|
||||||
List of all invoices
|
List of all invoices
|
||||||
--------------------
|
--------------------
|
||||||
@@ -203,6 +208,7 @@ List of all invoices
|
|||||||
"gross_value": "23.00",
|
"gross_value": "23.00",
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
"tax_name": "VAT",
|
"tax_name": "VAT",
|
||||||
|
"tax_code": "S/standard",
|
||||||
"tax_rate": "0.00"
|
"tax_rate": "0.00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -342,6 +348,7 @@ Fetching individual invoices
|
|||||||
"gross_value": "23.00",
|
"gross_value": "23.00",
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
"tax_name": "VAT",
|
"tax_name": "VAT",
|
||||||
|
"tax_code": "S/standard",
|
||||||
"tax_rate": "0.00"
|
"tax_rate": "0.00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -352,12 +359,12 @@ Fetching individual invoices
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param invoice_no: The ``invoice_no`` field of the invoice to fetch
|
:param number: The ``number`` field of the invoice to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/download/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/download/
|
||||||
|
|
||||||
Download an invoice in PDF format.
|
Download an invoice in PDF format.
|
||||||
|
|
||||||
@@ -384,7 +391,7 @@ Fetching individual invoices
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param invoice_no: The ``invoice_no`` field of the invoice to fetch
|
:param number: The ``number`` field of the invoice to fetch
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
@@ -397,7 +404,7 @@ Modifying invoices
|
|||||||
|
|
||||||
Invoices cannot be edited directly, but the following actions can be triggered:
|
Invoices cannot be edited directly, but the following actions can be triggered:
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/reissue/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/reissue/
|
||||||
|
|
||||||
Cancels the invoice and creates a new one.
|
Cancels the invoice and creates a new one.
|
||||||
|
|
||||||
@@ -419,13 +426,13 @@ Invoices cannot be edited directly, but the following actions can be triggered:
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param invoice_no: The ``invoice_no`` field of the invoice to reissue
|
:param number: The ``number`` field of the invoice to reissue
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 400: The invoice has already been canceled
|
:statuscode 400: The invoice has already been canceled
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/regenerate/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/regenerate/
|
||||||
|
|
||||||
Re-generates the invoice from order data.
|
Re-generates the invoice from order data.
|
||||||
|
|
||||||
@@ -447,7 +454,7 @@ Invoices cannot be edited directly, but the following actions can be triggered:
|
|||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
:param invoice_no: The ``invoice_no`` field of the invoice to regenerate
|
:param number: The ``number`` field of the invoice to regenerate
|
||||||
:statuscode 200: no error
|
:statuscode 200: no error
|
||||||
:statuscode 400: The invoice has already been canceled
|
:statuscode 400: The invoice has already been canceled
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ fees list of objects List of fees in
|
|||||||
├ tax_rate decimal (string) VAT rate applied for this fee
|
├ tax_rate decimal (string) VAT rate applied for this fee
|
||||||
├ tax_value money (string) VAT included in this fee
|
├ tax_value money (string) VAT included in this fee
|
||||||
├ tax_rule integer The ID of the used tax rule (or ``null``)
|
├ tax_rule integer The ID of the used tax rule (or ``null``)
|
||||||
|
├ tax_code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||||
└ canceled boolean Whether or not this fee has been canceled.
|
└ canceled boolean Whether or not this fee has been canceled.
|
||||||
downloads list of objects List of ticket download options for order-wise ticket
|
downloads list of objects List of ticket download options for order-wise ticket
|
||||||
downloading. This might be a multi-page PDF or a ZIP
|
downloading. This might be a multi-page PDF or a ZIP
|
||||||
@@ -104,6 +105,10 @@ url string The full URL to
|
|||||||
payments list of objects List of payment processes (see below)
|
payments list of objects List of payment processes (see below)
|
||||||
refunds list of objects List of refund processes (see below)
|
refunds list of objects List of refund processes (see below)
|
||||||
last_modified datetime Last modification of this object
|
last_modified datetime Last modification of this object
|
||||||
|
cancellation_date datetime Time of order cancellation (or ``null``). **Note**:
|
||||||
|
Will not be set for partial cancellations and is not
|
||||||
|
reliable for orders that have been cancelled,
|
||||||
|
reactivated and cancelled again.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -151,6 +156,13 @@ last_modified datetime Last modificati
|
|||||||
|
|
||||||
The ``expires`` attribute can now be passed during order creation.
|
The ``expires`` attribute can now be passed during order creation.
|
||||||
|
|
||||||
|
.. versionchanged:: 2024.11
|
||||||
|
|
||||||
|
The ``cancellation_date`` attribute has been added and can also be used as an ordering key.
|
||||||
|
|
||||||
|
.. versionchanged:: 2025.1
|
||||||
|
|
||||||
|
The ``tax_code`` attribute has been added.
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
@@ -188,6 +200,7 @@ voucher_budget_use money (string) Amount of money
|
|||||||
are changed *after* the order was created. Can be ``null``.
|
are changed *after* the order was created. Can be ``null``.
|
||||||
tax_rate decimal (string) VAT rate applied for this position
|
tax_rate decimal (string) VAT rate applied for this position
|
||||||
tax_value money (string) VAT included in this position
|
tax_value money (string) VAT included in this position
|
||||||
|
tax_code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||||
tax_rule integer The ID of the used tax rule (or ``null``)
|
tax_rule integer The ID of the used tax rule (or ``null``)
|
||||||
secret string Secret code printed on the tickets for validation
|
secret string Secret code printed on the tickets for validation
|
||||||
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
||||||
@@ -206,6 +219,17 @@ checkins list of objects List of **succe
|
|||||||
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
|
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
|
||||||
├ device_id integer Attribute ``device_id`` of the device. Can be ``null``.
|
├ device_id integer Attribute ``device_id`` of the device. Can be ``null``.
|
||||||
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
||||||
|
print_logs list of objects List of print jobs recorded e.g. by the pretix apps
|
||||||
|
├ id integer Internal ID of the print job
|
||||||
|
├ successful boolean Whether the print job successfully resulted in a print.
|
||||||
|
This is not expected to be 100 % reliable information (since
|
||||||
|
printer feedback is never perfect) and there is no guarantee
|
||||||
|
that unsuccessful jobs will be logged.
|
||||||
|
├ device_id integer Attribute ``device_id`` of the device that recorded the print. Can be ``null``.
|
||||||
|
├ datetime datetime Time of printing
|
||||||
|
├ source string Source of print job, e.g. name of the app used.
|
||||||
|
├ type string Type of print (currently ``badge``, ``ticket``, ``certificate``, or ``other``)
|
||||||
|
└ info object Additional data with client-dependent structure.
|
||||||
downloads list of objects List of ticket download options
|
downloads list of objects List of ticket download options
|
||||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||||
└ url string Download URL
|
└ url string Download URL
|
||||||
@@ -233,6 +257,14 @@ pdf_data object Data object req
|
|||||||
|
|
||||||
The attributes ``blocked``, ``valid_from`` and ``valid_until`` have been added.
|
The attributes ``blocked``, ``valid_from`` and ``valid_until`` have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2024.9
|
||||||
|
|
||||||
|
The attribute ``print_logs`` has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2025.1
|
||||||
|
|
||||||
|
The ``tax_code`` attribute has been added.
|
||||||
|
|
||||||
.. _order-payment-resource:
|
.. _order-payment-resource:
|
||||||
|
|
||||||
Order payment resource
|
Order payment resource
|
||||||
@@ -384,6 +416,7 @@ List of all orders
|
|||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
|
"tax_code": null,
|
||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
@@ -399,10 +432,21 @@ List of all orders
|
|||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
"device": 2,
|
"device": 2,
|
||||||
|
"device_id": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
"auto_checked_in": false
|
"auto_checked_in": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"print_logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "badge",
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"device_id": 1,
|
||||||
|
"source": "pretixSCAN",
|
||||||
|
"info": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
@@ -438,14 +482,15 @@ List of all orders
|
|||||||
"provider": "banktransfer"
|
"provider": "banktransfer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refunds": []
|
"refunds": [],
|
||||||
|
"cancellation_date": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``,
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``,
|
||||||
``last_modified``, and ``status``. Default: ``datetime``
|
``last_modified``, ``status`` and ``cancellation_date``. Default: ``datetime``
|
||||||
:query string code: Only return orders that match the given order code
|
:query string code: Only return orders that match the given order code
|
||||||
:query string status: Only return orders in the given order status (see above)
|
:query string status: Only return orders in the given order status (see above)
|
||||||
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
|
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
|
||||||
@@ -611,6 +656,7 @@ Fetching individual orders
|
|||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
|
"tax_code": null,
|
||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
@@ -626,10 +672,22 @@ Fetching individual orders
|
|||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
"device": 2,
|
"device": 2,
|
||||||
|
"device_id": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
"auto_checked_in": false
|
"auto_checked_in": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"print_logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "badge",
|
||||||
|
"successful": true,
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"device_id": 1,
|
||||||
|
"source": "pretixSCAN",
|
||||||
|
"info": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
@@ -665,7 +723,8 @@ Fetching individual orders
|
|||||||
"provider": "banktransfer"
|
"provider": "banktransfer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refunds": []
|
"refunds": [],
|
||||||
|
"cancellation_date": null
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -796,7 +855,7 @@ Generating new secrets
|
|||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/regenerate_secrets/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/regenerate_secrets/
|
||||||
|
|
||||||
Triggers generation of new ``secret`` attributes for both the order and all order positions.
|
Triggers generation of new ``secret`` and ``ẁeb_secret`` attributes for both the order and all order positions.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@@ -827,7 +886,7 @@ Generating new secrets
|
|||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/regenerate_secrets/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/regenerate_secrets/
|
||||||
|
|
||||||
Triggers generation of a new ``secret`` attribute for a single order position.
|
Triggers generation of a new ``secret`` and ``web_secret`` attribute for a single order position.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@@ -977,8 +1036,8 @@ Creating orders
|
|||||||
* ``internal_reference``
|
* ``internal_reference``
|
||||||
* ``vat_id``
|
* ``vat_id``
|
||||||
* ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check
|
* ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check
|
||||||
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
|
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
|
||||||
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
||||||
|
|
||||||
* ``positions``
|
* ``positions``
|
||||||
|
|
||||||
@@ -1566,6 +1625,7 @@ List of all order positions
|
|||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
|
"tax_code": null,
|
||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"discount": null,
|
"discount": null,
|
||||||
"pseudonymization_id": "MQLJvANO3B",
|
"pseudonymization_id": "MQLJvANO3B",
|
||||||
@@ -1581,10 +1641,22 @@ List of all order positions
|
|||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
"device": 2,
|
"device": 2,
|
||||||
|
"device_id": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
"auto_checked_in": false
|
"auto_checked_in": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"print_logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "badge",
|
||||||
|
"successful": true,
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"device_id": 1,
|
||||||
|
"source": "pretixSCAN",
|
||||||
|
"info": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
@@ -1680,6 +1752,7 @@ Fetching individual positions
|
|||||||
"tax_rate": "0.00",
|
"tax_rate": "0.00",
|
||||||
"tax_rule": null,
|
"tax_rule": null,
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
|
"tax_code": null,
|
||||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
"addon_to": null,
|
"addon_to": null,
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
@@ -1695,10 +1768,22 @@ Fetching individual positions
|
|||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
"device": 2,
|
"device": 2,
|
||||||
|
"device_id": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
"auto_checked_in": false
|
"auto_checked_in": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"print_logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "badge",
|
||||||
|
"successful": true,
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"device_id": 1,
|
||||||
|
"source": "pretixSCAN",
|
||||||
|
"info": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
@@ -1795,6 +1880,10 @@ Manipulating individual positions
|
|||||||
|
|
||||||
The endpoints to manage blocks have been added.
|
The endpoints to manage blocks have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2024.9
|
||||||
|
|
||||||
|
The API now supports logging ticket and badge prints.
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
||||||
|
|
||||||
Updates specific fields on an order position. Currently, only the following fields are supported:
|
Updates specific fields on an order position. Currently, only the following fields are supported:
|
||||||
@@ -2054,6 +2143,59 @@ Manipulating individual positions
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/printlog/
|
||||||
|
|
||||||
|
Creates a print log, stating that this ticket has been printed.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/printlog/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"datetime": "2024-09-19T13:37:00+02:00",
|
||||||
|
"source": "pretixPOS",
|
||||||
|
"type": "badge",
|
||||||
|
"info": {
|
||||||
|
"cashier": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/pdf
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1234,
|
||||||
|
"device_id": null,
|
||||||
|
"datetime": "2024-09-19T13:37:00+02:00",
|
||||||
|
"source": "pretixPOS",
|
||||||
|
"type": "badge",
|
||||||
|
"info": {
|
||||||
|
"cashier": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create a log for
|
||||||
|
:param event: The ``slug`` field of the event to create a log for
|
||||||
|
:param id: The ``id`` field of the order position to create a log for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
|
||||||
|
**or** downloads are not available for this order position at this time. The response content will
|
||||||
|
contain more details.
|
||||||
|
:statuscode 404: The requested order position or download provider does not exist.
|
||||||
|
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
||||||
|
seconds.
|
||||||
|
|
||||||
Changing order contents
|
Changing order contents
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ Endpoints for event exports
|
|||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
||||||
|
|
||||||
Endpoints for organizer exports
|
Endpoints for organizer exports
|
||||||
---------------------------
|
-------------------------------
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/scheduled_exports/
|
.. http:get:: /api/v1/organizers/(organizer)/scheduled_exports/
|
||||||
|
|
||||||
@@ -553,4 +553,4 @@ Endpoints for organizer exports
|
|||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
|
||||||
|
|
||||||
|
|
||||||
.. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3
|
.. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3
|
||||||
|
|||||||
+113
-2
@@ -1,4 +1,4 @@
|
|||||||
.. _`rest-reusablemedia`:
|
.. _`rest-seats`:
|
||||||
|
|
||||||
Seats
|
Seats
|
||||||
=====
|
=====
|
||||||
@@ -249,7 +249,7 @@ Endpoints
|
|||||||
"orderposition": null,
|
"orderposition": null,
|
||||||
"cartposition": null,
|
"cartposition": null,
|
||||||
"voucher": null
|
"voucher": null
|
||||||
},
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
:param event: The ``slug`` field of the event to modify
|
:param event: The ``slug`` field of the event to modify
|
||||||
@@ -260,3 +260,114 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
|
||||||
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
|
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/seats/bulk_block/
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/bulk_block/
|
||||||
|
|
||||||
|
Set the ``blocked`` attribute to ``true`` for a large number of seats at once.
|
||||||
|
You can pass either a list of ``id`` values or a list of ``seat_guid`` values.
|
||||||
|
You can pass up to 10,000 seats in one request.
|
||||||
|
|
||||||
|
The endpoint will return an error if you pass a seat ID that does not exist.
|
||||||
|
However, it will not return an error if one of the passed seats is already blocked or sold.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_block/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"ids": [12, 45, 56]
|
||||||
|
}
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_block/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"seat_guids": ["6c0e29e5-05d6-421f-99f3-afd01478ecad", "c2899340-e2e7-4d05-8100-000a4b6d7cf4"]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The seat could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
|
||||||
|
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/seats/bulk_unblock/
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/seats/bulk_unblock/
|
||||||
|
|
||||||
|
Set the ``blocked`` attribute to ``false`` for a large number of seats at once.
|
||||||
|
You can pass either a list of ``id`` values or a list of ``seat_guid`` values.
|
||||||
|
You can pass up to 10,000 seats in one request.
|
||||||
|
|
||||||
|
The endpoint will return an error if you pass a seat ID that does not exist.
|
||||||
|
However, it will not return an error if one of the passed seat is already unblocked or is sold.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_unblock/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"ids": [12, 45, 56]
|
||||||
|
}
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PATCH /api/v1/organizers/bigevents/events/sampleconf/seats/bulk_unblock/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"seat_guids": ["6c0e29e5-05d6-421f-99f3-afd01478ecad", "c2899340-e2e7-4d05-8100-000a4b6d7cf4"]
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
:param event: The ``slug`` field of the event to modify
|
||||||
|
:param subevent_id: The ``id`` field of the subevent to modify
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 400: The seat could not be modified due to invalid submitted data
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to change this resource.
|
||||||
|
:statuscode 404: Seat does not exist; or the endpoint without subevent id was used for event with subevents, or vice versa.
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
.. spelling:word-list::
|
||||||
|
|
||||||
|
EN16931
|
||||||
|
DSFinV-K
|
||||||
|
|
||||||
.. _rest-taxrules:
|
.. _rest-taxrules:
|
||||||
|
|
||||||
Tax rules
|
Tax rules
|
||||||
@@ -18,6 +23,7 @@ id integer Internal ID of
|
|||||||
name multi-lingual string The tax rules' name
|
name multi-lingual string The tax rules' name
|
||||||
internal_name string An optional name that is only used in the backend
|
internal_name string An optional name that is only used in the backend
|
||||||
rate decimal (string) Tax rate in percent
|
rate decimal (string) Tax rate in percent
|
||||||
|
code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||||
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
||||||
the specified product price
|
the specified product price
|
||||||
eu_reverse_charge boolean **DEPRECATED**. If ``true``, EU reverse charge rules
|
eu_reverse_charge boolean **DEPRECATED**. If ``true``, EU reverse charge rules
|
||||||
@@ -42,6 +48,42 @@ custom_rules object Dynamic rules s
|
|||||||
|
|
||||||
The ``custom_rules`` attribute has been added.
|
The ``custom_rules`` attribute has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2023.8
|
||||||
|
|
||||||
|
The ``code`` attribute has been added.
|
||||||
|
|
||||||
|
.. _rest-taxcodes:
|
||||||
|
|
||||||
|
Tax codes
|
||||||
|
---------
|
||||||
|
|
||||||
|
For integration with external systems, such as electronic invoicing or bookkeeping systems, the tax rate itself is often
|
||||||
|
not sufficient information. For example, there could be many different reasons why a sale has a tax rate of 0 %, but the
|
||||||
|
external handling of the transaction depends on which reason applies. Therefore, pretix allows to supply a codified
|
||||||
|
reason that allows us to understand what the specific legal situation is. These tax codes are modeled after a combination
|
||||||
|
of the code lists from the European standard EN16931 and the German standard DSFinV-K.
|
||||||
|
|
||||||
|
The following codes are supported:
|
||||||
|
|
||||||
|
- ``S/standard`` -- Standard VAT rate in the merchant country
|
||||||
|
- ``S/reduced`` -- Reduced VAT rate in the merchant country
|
||||||
|
- ``S/averaged`` -- Averaged VAT rate in the merchant country (known use case: agricultural businesses in Germany)
|
||||||
|
- ``AE`` -- Reverse charge
|
||||||
|
- ``O`` -- Services outside of scope of tax
|
||||||
|
- ``E`` -- Exempt from tax (no reason given)
|
||||||
|
- ``E/<reason>`` -- Exempt from tax, where ``<reason>`` is one of the codes listed in the `VATEX code list`_ version 5.0.
|
||||||
|
- ``Z`` -- Zero-rated goods
|
||||||
|
- ``G`` -- Free export item, VAT not charged
|
||||||
|
- ``K`` -- VAT exempt for EEA intra-community supply of goods and services
|
||||||
|
- ``L`` -- Canary Islands general indirect tax
|
||||||
|
- ``M`` -- Tax for production, services and importation in Ceuta and Melilla
|
||||||
|
- ``B`` -- Transferred (VAT), only in Italy
|
||||||
|
|
||||||
|
The code set in the ``code`` attribute of the tax rule is used by default. When ``eu_reverse_charge`` is active, the
|
||||||
|
code is replaced by ``AE`` for reverse charge sales and by ``O`` for non-EU sales. When configuring custom rules, you
|
||||||
|
should actively set a ``"code"`` key on each rule. Only for ``"action": "reverse"`` we automatically apply the code
|
||||||
|
``AE``, in all other cases the default ``code`` of the tax rule is selected.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -74,6 +116,7 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "VAT"},
|
"name": {"en": "VAT"},
|
||||||
"internal_name": "VAT",
|
"internal_name": "VAT",
|
||||||
|
"code": "S/standard",
|
||||||
"rate": "19.00",
|
"rate": "19.00",
|
||||||
"price_includes_tax": true,
|
"price_includes_tax": true,
|
||||||
"eu_reverse_charge": false,
|
"eu_reverse_charge": false,
|
||||||
@@ -115,6 +158,7 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "VAT"},
|
"name": {"en": "VAT"},
|
||||||
"internal_name": "VAT",
|
"internal_name": "VAT",
|
||||||
|
"code": "S/standard",
|
||||||
"rate": "19.00",
|
"rate": "19.00",
|
||||||
"price_includes_tax": true,
|
"price_includes_tax": true,
|
||||||
"eu_reverse_charge": false,
|
"eu_reverse_charge": false,
|
||||||
@@ -164,6 +208,7 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "VAT"},
|
"name": {"en": "VAT"},
|
||||||
"internal_name": "VAT",
|
"internal_name": "VAT",
|
||||||
|
"code": "S/standard",
|
||||||
"rate": "19.00",
|
"rate": "19.00",
|
||||||
"price_includes_tax": true,
|
"price_includes_tax": true,
|
||||||
"eu_reverse_charge": false,
|
"eu_reverse_charge": false,
|
||||||
@@ -212,6 +257,7 @@ Endpoints
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "VAT"},
|
"name": {"en": "VAT"},
|
||||||
"internal_name": "VAT",
|
"internal_name": "VAT",
|
||||||
|
"code": "S/standard",
|
||||||
"rate": "20.00",
|
"rate": "20.00",
|
||||||
"price_includes_tax": true,
|
"price_includes_tax": true,
|
||||||
"eu_reverse_charge": false,
|
"eu_reverse_charge": false,
|
||||||
@@ -258,3 +304,4 @@ Endpoints
|
|||||||
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use.
|
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use.
|
||||||
|
|
||||||
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/tax-rules-custom.schema.json
|
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/tax-rules-custom.schema.json
|
||||||
|
.. _VATEX code list: https://ec.europa.eu/digital-building-blocks/sites/display/DIGITAL/Registry+of+supporting+artefacts+to+implement+EN16931#RegistryofsupportingartefactstoimplementEN16931-Codelists
|
||||||
@@ -17,6 +17,7 @@ First, you need to declare that you are using non-essential cookies by respondin
|
|||||||
signal:
|
signal:
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
:no-index:
|
||||||
:members: register_cookie_providers
|
:members: register_cookie_providers
|
||||||
|
|
||||||
You are expected to return a list of ``CookieProvider`` objects instantiated from the following class:
|
You are expected to return a list of ``CookieProvider`` objects instantiated from the following class:
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ Order events
|
|||||||
There are multiple signals that will be sent out in the ordering cycle:
|
There are multiple signals that will be sent out in the ordering cycle:
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
: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_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
||||||
|
|
||||||
Check-ins
|
Check-ins
|
||||||
"""""""""
|
"""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: checkin_created
|
:members: checkin_created
|
||||||
|
|
||||||
|
|
||||||
@@ -39,18 +41,21 @@ Frontend
|
|||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
:no-index:
|
||||||
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
|
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
|
||||||
|
|
||||||
Request flow
|
Request flow
|
||||||
""""""""""""
|
""""""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
:no-index:
|
||||||
:members: process_request, process_response
|
:members: process_request, process_response
|
||||||
|
|
||||||
Vouchers
|
Vouchers
|
||||||
""""""""
|
""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
:no-index:
|
||||||
:members: voucher_redeem_info
|
:members: voucher_redeem_info
|
||||||
|
|
||||||
Backend
|
Backend
|
||||||
@@ -62,24 +67,28 @@ Backend
|
|||||||
item_formsets, order_search_filter_q, order_search_forms
|
item_formsets, order_search_filter_q, order_search_forms
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
|
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
|
||||||
|
|
||||||
Vouchers
|
Vouchers
|
||||||
""""""""
|
""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
|
:no-index:
|
||||||
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
|
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
|
||||||
|
|
||||||
Dashboards
|
Dashboards
|
||||||
""""""""""
|
""""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
|
:no-index:
|
||||||
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
|
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
|
||||||
|
|
||||||
Ticket designs
|
Ticket designs
|
||||||
""""""""""""""
|
""""""""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: layout_text_variables, layout_image_variables
|
:members: layout_text_variables, layout_image_variables
|
||||||
|
|
||||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
||||||
@@ -89,4 +98,9 @@ API
|
|||||||
---
|
---
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: validate_event_settings, api_event_settings_fields
|
:members: validate_event_settings, api_event_settings_fields
|
||||||
|
|
||||||
|
.. automodule:: pretix.api.signals
|
||||||
|
:no-index:
|
||||||
|
:members: register_device_security_profile
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ that we'll provide in this plugin:
|
|||||||
Similar signals exist for other objects:
|
Similar signals exist for other objects:
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: voucher_import_columns
|
:members: voucher_import_columns
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -84,8 +84,6 @@ convenient to you:
|
|||||||
|
|
||||||
.. automethod:: _register_fonts
|
.. automethod:: _register_fonts
|
||||||
|
|
||||||
.. automethod:: _register_event_fonts
|
|
||||||
|
|
||||||
.. automethod:: _on_first_page
|
.. automethod:: _on_first_page
|
||||||
|
|
||||||
.. automethod:: _on_other_page
|
.. automethod:: _on_other_page
|
||||||
|
|||||||
@@ -86,7 +86,10 @@ Signals
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: register_text_placeholders
|
:members: register_text_placeholders
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: register_mail_placeholders
|
:members: register_mail_placeholders
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
KulturPass
|
KulturPass
|
||||||
=========
|
==========
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ expects and - more importantly - supports.
|
|||||||
for a sample configuration in an academic context.
|
for a sample configuration in an academic context.
|
||||||
|
|
||||||
Note, that you can have multiple attributes with the same ``friendlyName``
|
Note, that you can have multiple attributes with the same ``friendlyName``
|
||||||
but different ``name``s. This is often used in systems, where the same
|
but different ``name`` value. This is often used in systems, where the same
|
||||||
information (for example a persons name) is saved in different fields -
|
information (for example a persons name) is saved in different fields -
|
||||||
for example because one institution is returning SAML 1.0 and other
|
for example because one institution is returning SAML 1.0 and other
|
||||||
institutions are returning SAML 2.0 style attributes. Typically, this only
|
institutions are returning SAML 2.0 style attributes. Typically, this only
|
||||||
|
|||||||
+15
-17
@@ -29,10 +29,10 @@ dependencies = [
|
|||||||
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
||||||
"babel",
|
"babel",
|
||||||
"BeautifulSoup4==4.12.*",
|
"BeautifulSoup4==4.12.*",
|
||||||
"bleach==5.0.*",
|
"bleach==6.2.*",
|
||||||
"celery==5.4.*",
|
"celery==5.4.*",
|
||||||
"chardet==5.2.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=3.4.2",
|
"cryptography>=44.0.0",
|
||||||
"css-inline==0.14.*",
|
"css-inline==0.14.*",
|
||||||
"defusedcsv>=1.1.0",
|
"defusedcsv>=1.1.0",
|
||||||
"Django[argon2]==4.2.*,>=4.2.15",
|
"Django[argon2]==4.2.*,>=4.2.15",
|
||||||
@@ -43,8 +43,8 @@ dependencies = [
|
|||||||
"django-formset-js-improved==0.5.0.3",
|
"django-formset-js-improved==0.5.0.3",
|
||||||
"django-formtools==2.5.1",
|
"django-formtools==2.5.1",
|
||||||
"django-hierarkey==1.2.*",
|
"django-hierarkey==1.2.*",
|
||||||
"django-hijack==3.6.*",
|
"django-hijack==3.7.*",
|
||||||
"django-i18nfield==1.9.*,>=1.9.4",
|
"django-i18nfield==1.10.*",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
"django-localflavor==4.0",
|
"django-localflavor==4.0",
|
||||||
"django-markup",
|
"django-markup",
|
||||||
@@ -53,7 +53,7 @@ dependencies = [
|
|||||||
"django-phonenumber-field==7.3.*",
|
"django-phonenumber-field==7.3.*",
|
||||||
"django-redis==5.4.*",
|
"django-redis==5.4.*",
|
||||||
"django-scopes==2.0.*",
|
"django-scopes==2.0.*",
|
||||||
"django-statici18n==2.5.*",
|
"django-statici18n==2.6.*",
|
||||||
"djangorestframework==3.15.*",
|
"djangorestframework==3.15.*",
|
||||||
"dnspython==2.7.*",
|
"dnspython==2.7.*",
|
||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
@@ -74,41 +74,40 @@ dependencies = [
|
|||||||
"paypal-checkout-serversdk==1.0.*",
|
"paypal-checkout-serversdk==1.0.*",
|
||||||
"PyJWT==2.9.*",
|
"PyJWT==2.9.*",
|
||||||
"phonenumberslite==8.13.*",
|
"phonenumberslite==8.13.*",
|
||||||
"Pillow==10.4.*",
|
"Pillow==11.1.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==5.28.*",
|
"protobuf==5.29.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.22",
|
"pycparser==2.22",
|
||||||
"pycryptodome==3.21.*",
|
"pycryptodome==3.21.*",
|
||||||
"pypdf==5.0.*",
|
"pypdf==5.1.*",
|
||||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||||
"python-dateutil==2.9.*",
|
"python-dateutil==2.9.*",
|
||||||
"pytz",
|
"pytz",
|
||||||
"pytz-deprecation-shim==0.1.*",
|
"pytz-deprecation-shim==0.1.*",
|
||||||
"pyuca",
|
"pyuca",
|
||||||
"qrcode==8.0",
|
"qrcode==8.0",
|
||||||
"redis==5.1.*",
|
"redis==5.2.*",
|
||||||
"reportlab==4.2.*",
|
"reportlab==4.2.*",
|
||||||
"requests==2.31.*",
|
"requests==2.31.*",
|
||||||
"sentry-sdk==2.17.*",
|
"sentry-sdk==2.18.*",
|
||||||
"sepaxml==2.6.*",
|
"sepaxml==2.6.*",
|
||||||
"slimit",
|
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
"tlds>=2020041600",
|
"tlds>=2020041600",
|
||||||
"tqdm==4.*",
|
"tqdm==4.*",
|
||||||
"ua-parser==0.18.*",
|
"ua-parser==1.0.*",
|
||||||
"vat_moss_forked==2020.3.20.0.11.0",
|
"vat_moss_forked==2020.3.20.0.11.0",
|
||||||
"vobject==0.9.*",
|
"vobject==0.9.*",
|
||||||
"webauthn==2.2.*",
|
"webauthn==2.4.*",
|
||||||
"zeep==4.2.*"
|
"zeep==4.3.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memcached = ["pylibmc"]
|
memcached = ["pylibmc"]
|
||||||
dev = [
|
dev = [
|
||||||
"aiohttp==3.10.*",
|
"aiohttp==3.11.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.26.*",
|
"fakeredis==2.26.*",
|
||||||
@@ -117,12 +116,11 @@ dev = [
|
|||||||
"isort==5.13.*",
|
"isort==5.13.*",
|
||||||
"pep8-naming==0.14.*",
|
"pep8-naming==0.14.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pytest-asyncio",
|
"pytest-asyncio>=0.24",
|
||||||
"pytest-cache",
|
"pytest-cache",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-django==4.*",
|
"pytest-django==4.*",
|
||||||
"pytest-mock==3.14.*",
|
"pytest-mock==3.14.*",
|
||||||
"pytest-rerunfailures==14.*",
|
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.6.*",
|
"pytest-xdist==3.6.*",
|
||||||
"pytest==8.3.*",
|
"pytest==8.3.*",
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2024.10.0.dev0"
|
__version__ = "2024.12.0.dev0"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from rest_framework import exceptions
|
|||||||
from rest_framework.authentication import TokenAuthentication
|
from rest_framework.authentication import TokenAuthentication
|
||||||
|
|
||||||
from pretix.api.auth.devicesecurity import (
|
from pretix.api.auth.devicesecurity import (
|
||||||
DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile,
|
FullAccessSecurityProfile, get_all_security_profiles,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Device
|
from pretix.base.models import Device
|
||||||
|
|
||||||
@@ -58,7 +58,8 @@ class DeviceTokenAuthentication(TokenAuthentication):
|
|||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
r = super().authenticate(request)
|
r = super().authenticate(request)
|
||||||
if r and isinstance(r[1], Device):
|
if r and isinstance(r[1], Device):
|
||||||
profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile)
|
profiles = get_all_security_profiles()
|
||||||
|
profile = profiles.get(r[1].security_profile, FullAccessSecurityProfile())
|
||||||
if not profile.is_allowed(request):
|
if not profile.is_allowed(request):
|
||||||
raise exceptions.PermissionDenied('Request denied by device security profile.')
|
raise exceptions.PermissionDenied('Request denied by device security profile.')
|
||||||
return r
|
return r
|
||||||
|
|||||||
@@ -20,13 +20,40 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from pretix.api.signals import register_device_security_profile
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
_ALL_PROFILES = None
|
||||||
|
|
||||||
|
|
||||||
class FullAccessSecurityProfile:
|
class BaseSecurityProfile:
|
||||||
|
@property
|
||||||
|
def identifier(self) -> str:
|
||||||
|
"""
|
||||||
|
Unique identifier for this profile.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self) -> str:
|
||||||
|
"""
|
||||||
|
Human-readable name (can be a ``gettext_lazy`` object).
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def is_allowed(self, request) -> bool:
|
||||||
|
"""
|
||||||
|
Return whether a given request should be allowed.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class FullAccessSecurityProfile(BaseSecurityProfile):
|
||||||
identifier = 'full'
|
identifier = 'full'
|
||||||
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
|
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
|
||||||
|
|
||||||
@@ -34,7 +61,7 @@ class FullAccessSecurityProfile:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class AllowListSecurityProfile:
|
class AllowListSecurityProfile(BaseSecurityProfile):
|
||||||
allowlist = ()
|
allowlist = ()
|
||||||
|
|
||||||
def is_allowed(self, request):
|
def is_allowed(self, request):
|
||||||
@@ -77,6 +104,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:blockedsecrets-list'),
|
('GET', 'api-v1:blockedsecrets-list'),
|
||||||
('GET', 'api-v1:order-list'),
|
('GET', 'api-v1:order-list'),
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
('GET', 'api-v1:orderposition-pdf_image'),
|
||||||
|
('POST', 'api-v1:orderposition-printlog'),
|
||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
@@ -112,6 +140,7 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:revokedsecrets-list'),
|
('GET', 'api-v1:revokedsecrets-list'),
|
||||||
('GET', 'api-v1:blockedsecrets-list'),
|
('GET', 'api-v1:blockedsecrets-list'),
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
('GET', 'api-v1:orderposition-pdf_image'),
|
||||||
|
('POST', 'api-v1:orderposition-printlog'),
|
||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
@@ -147,6 +176,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:revokedsecrets-list'),
|
('GET', 'api-v1:revokedsecrets-list'),
|
||||||
('GET', 'api-v1:blockedsecrets-list'),
|
('GET', 'api-v1:blockedsecrets-list'),
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
('GET', 'api-v1:orderposition-pdf_image'),
|
||||||
|
('POST', 'api-v1:orderposition-printlog'),
|
||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
@@ -154,87 +184,28 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PretixPosSecurityProfile(AllowListSecurityProfile):
|
def get_all_security_profiles():
|
||||||
identifier = 'pretixpos'
|
global _ALL_PROFILES
|
||||||
verbose_name = _('pretixPOS')
|
|
||||||
allowlist = (
|
if _ALL_PROFILES:
|
||||||
('GET', 'api-v1:version'),
|
return _ALL_PROFILES
|
||||||
('GET', 'api-v1:device.eventselection'),
|
|
||||||
('GET', 'api-v1:idempotency.query'),
|
types = OrderedDict()
|
||||||
('GET', 'api-v1:device.info'),
|
for recv, ret in register_device_security_profile.send(None):
|
||||||
('POST', 'api-v1:device.update'),
|
if isinstance(ret, (list, tuple)):
|
||||||
('POST', 'api-v1:device.revoke'),
|
for r in ret:
|
||||||
('POST', 'api-v1:device.roll'),
|
types[r.identifier] = r
|
||||||
('GET', 'api-v1:event-list'),
|
else:
|
||||||
('GET', 'api-v1:event-detail'),
|
types[ret.identifier] = ret
|
||||||
('GET', 'api-v1:subevent-list'),
|
_ALL_PROFILES = types
|
||||||
('GET', 'api-v1:subevent-detail'),
|
return types
|
||||||
('GET', 'api-v1:itemcategory-list'),
|
|
||||||
('GET', 'api-v1:item-list'),
|
|
||||||
('GET', 'api-v1:question-list'),
|
|
||||||
('GET', 'api-v1:quota-list'),
|
|
||||||
('GET', 'api-v1:taxrule-list'),
|
|
||||||
('GET', 'api-v1:ticketlayout-list'),
|
|
||||||
('GET', 'api-v1:ticketlayoutitem-list'),
|
|
||||||
('GET', 'api-v1:badgelayout-list'),
|
|
||||||
('GET', 'api-v1:badgeitem-list'),
|
|
||||||
('GET', 'api-v1:voucher-list'),
|
|
||||||
('GET', 'api-v1:voucher-detail'),
|
|
||||||
('GET', 'api-v1:order-list'),
|
|
||||||
('POST', 'api-v1:order-list'),
|
|
||||||
('GET', 'api-v1:order-detail'),
|
|
||||||
('DELETE', 'api-v1:orderposition-detail'),
|
|
||||||
('PATCH', 'api-v1:orderposition-detail'),
|
|
||||||
('GET', 'api-v1:orderposition-list'),
|
|
||||||
('GET', 'api-v1:orderposition-answer'),
|
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
|
||||||
('POST', 'api-v1:order-mark-canceled'),
|
|
||||||
('POST', 'api-v1:orderpayment-list'),
|
|
||||||
('POST', 'api-v1:orderrefund-list'),
|
|
||||||
('POST', 'api-v1:orderrefund-done'),
|
|
||||||
('POST', 'api-v1:cartposition-list'),
|
|
||||||
('POST', 'api-v1:cartposition-bulk-create'),
|
|
||||||
('GET', 'api-v1:checkinlist-list'),
|
|
||||||
('POST', 'api-v1:checkinlistpos-redeem'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:order.posprintlog'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:order.poslock'),
|
|
||||||
('DELETE', 'plugins:pretix_posbackend:order.poslock'),
|
|
||||||
('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'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:posdebuglogentry-list'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:posdebuglogentry-bulk-create'),
|
|
||||||
('GET', 'plugins:pretix_posbackend:poscashier-list'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
|
|
||||||
('POST', 'plugins:pretix_posbackend:stripeterminal.paymentintent'),
|
|
||||||
('PUT', 'plugins:pretix_posbackend:file.upload'),
|
|
||||||
('GET', 'api-v1:revokedsecrets-list'),
|
|
||||||
('GET', 'api-v1:blockedsecrets-list'),
|
|
||||||
('GET', 'api-v1:event.settings'),
|
|
||||||
('GET', 'plugins:pretix_seating:event.event'),
|
|
||||||
('GET', 'plugins:pretix_seating:event.event.subevent'),
|
|
||||||
('GET', 'plugins:pretix_seating:event.plan'),
|
|
||||||
('GET', 'plugins:pretix_seating:selection.simple'),
|
|
||||||
('POST', 'api-v1:upload'),
|
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
|
||||||
('GET', 'api-v1:checkinrpc.search'),
|
|
||||||
('POST', 'api-v1:reusablemedium-lookup'),
|
|
||||||
('GET', 'api-v1:reusablemedium-list'),
|
|
||||||
('POST', 'api-v1:reusablemedium-list'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
DEVICE_SECURITY_PROFILES = {
|
@receiver(register_device_security_profile, dispatch_uid="base_register_default_security_profiles")
|
||||||
k.identifier: k() for k in (
|
def register_default_webhook_events(sender, **kwargs):
|
||||||
FullAccessSecurityProfile,
|
return (
|
||||||
PretixScanSecurityProfile,
|
FullAccessSecurityProfile(),
|
||||||
PretixScanNoSyncSecurityProfile,
|
PretixScanSecurityProfile(),
|
||||||
PretixScanNoSyncNoSearchSecurityProfile,
|
PretixScanNoSyncSecurityProfile(),
|
||||||
PretixPosSecurityProfile,
|
PretixScanNoSyncNoSearchSecurityProfile(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class SalesChannelMigrationMixin:
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
if data["sales_channels"] == all_channels:
|
if set(data["sales_channels"]) == all_channels:
|
||||||
data["all_sales_channels"] = True
|
data["all_sales_channels"] = True
|
||||||
data["limit_sales_channels"] = []
|
data["limit_sales_channels"] = []
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
|||||||
return cid
|
return cid
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data.pop('sales_channel')
|
validated_data.pop('sales_channel', None)
|
||||||
addons_data = validated_data.pop('addons', None)
|
addons_data = validated_data.pop('addons', None)
|
||||||
bundled_data = validated_data.pop('bundled', None)
|
bundled_data = validated_data.pop('bundled', None)
|
||||||
|
|
||||||
|
|||||||
@@ -26,31 +26,22 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import Checkin, CheckinList, SalesChannel
|
from pretix.base.models import Checkin, CheckinList
|
||||||
|
|
||||||
|
|
||||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||||
checkin_count = serializers.IntegerField(read_only=True)
|
checkin_count = serializers.IntegerField(read_only=True)
|
||||||
position_count = serializers.IntegerField(read_only=True)
|
position_count = serializers.IntegerField(read_only=True)
|
||||||
auto_checkin_sales_channels = serializers.SlugRelatedField(
|
|
||||||
slug_field="identifier",
|
|
||||||
queryset=SalesChannel.objects.none(),
|
|
||||||
required=False,
|
|
||||||
allow_empty=True,
|
|
||||||
many=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||||
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
|
'include_pending', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||||
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
|
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
|
||||||
|
|
||||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import PermissionDenied, ValidationError
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -43,6 +43,7 @@ from django.utils.translation import gettext as _
|
|||||||
from django_countries.serializers import CountryFieldMixin
|
from django_countries.serializers import CountryFieldMixin
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import ChoiceField, Field
|
from rest_framework.fields import ChoiceField, Field
|
||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
|
|
||||||
@@ -436,7 +437,8 @@ class CloneEventSerializer(EventSerializer):
|
|||||||
testmode = validated_data.pop('testmode', None)
|
testmode = validated_data.pop('testmode', None)
|
||||||
has_subevents = validated_data.pop('has_subevents', None)
|
has_subevents = validated_data.pop('has_subevents', None)
|
||||||
tz = validated_data.pop('timezone', None)
|
tz = validated_data.pop('timezone', None)
|
||||||
sales_channels = validated_data.pop('sales_channels', None)
|
all_sales_channels = validated_data.pop('all_sales_channels', None)
|
||||||
|
limit_sales_channels = validated_data.pop('limit_sales_channels', None)
|
||||||
date_admission = validated_data.pop('date_admission', None)
|
date_admission = validated_data.pop('date_admission', None)
|
||||||
new_event = super().create({**validated_data, 'plugins': None})
|
new_event = super().create({**validated_data, 'plugins': None})
|
||||||
|
|
||||||
@@ -449,8 +451,9 @@ class CloneEventSerializer(EventSerializer):
|
|||||||
new_event.is_public = is_public
|
new_event.is_public = is_public
|
||||||
if testmode is not None:
|
if testmode is not None:
|
||||||
new_event.testmode = testmode
|
new_event.testmode = testmode
|
||||||
if sales_channels is not None:
|
if all_sales_channels is not None or limit_sales_channels is not None:
|
||||||
new_event.sales_channels = sales_channels
|
new_event.all_sales_channels = all_sales_channels
|
||||||
|
new_event.limit_sales_channels.set(limit_sales_channels)
|
||||||
if has_subevents is not None:
|
if has_subevents is not None:
|
||||||
new_event.has_subevents = has_subevents
|
new_event.has_subevents = has_subevents
|
||||||
if has_subevents is not None:
|
if has_subevents is not None:
|
||||||
@@ -678,8 +681,8 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TaxRule
|
model = TaxRule
|
||||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name',
|
fields = ('id', 'name', 'rate', 'code', 'price_includes_tax', 'eu_reverse_charge', 'home_country',
|
||||||
'keep_gross_if_rate_changes', 'custom_rules')
|
'internal_name', 'keep_gross_if_rate_changes', 'custom_rules')
|
||||||
|
|
||||||
|
|
||||||
class EventSettingsSerializer(SettingsSerializer):
|
class EventSettingsSerializer(SettingsSerializer):
|
||||||
@@ -989,6 +992,40 @@ def prefetch_by_id(items, qs, id_attr, target_attr):
|
|||||||
setattr(item, target_attr, result.get(getattr(item, id_attr)))
|
setattr(item, target_attr, result.get(getattr(item, id_attr)))
|
||||||
|
|
||||||
|
|
||||||
|
class SeatBulkBlockInputSerializer(serializers.Serializer):
|
||||||
|
ids = serializers.ListField(child=serializers.IntegerField(), required=False, allow_empty=True)
|
||||||
|
seat_guids = serializers.ListField(child=serializers.CharField(), required=False, allow_empty=True)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
data = super().to_internal_value(data)
|
||||||
|
|
||||||
|
if data.get("seat_guids") and data.get("ids"):
|
||||||
|
raise ValidationError("Please pass either seat_guids or ids.")
|
||||||
|
|
||||||
|
if data.get("seat_guids"):
|
||||||
|
seat_ids = data["seat_guids"]
|
||||||
|
if len(seat_ids) > 10000:
|
||||||
|
raise ValidationError({"seat_guids": ["Please do not pass over 10000 seats."]})
|
||||||
|
|
||||||
|
seats = {s.seat_guid: s for s in self.context["queryset"].filter(seat_guid__in=seat_ids)}
|
||||||
|
for s in seat_ids:
|
||||||
|
if s not in seats:
|
||||||
|
raise ValidationError({"seat_guids": [f"The seat '{s}' does not exist."]})
|
||||||
|
elif data.get("ids"):
|
||||||
|
seat_ids = data["ids"]
|
||||||
|
if len(seat_ids) > 10000:
|
||||||
|
raise ValidationError({"ids": ["Please do not pass over 10000 seats."]})
|
||||||
|
|
||||||
|
seats = self.context["queryset"].in_bulk(seat_ids)
|
||||||
|
for s in seat_ids:
|
||||||
|
if s not in seats:
|
||||||
|
raise ValidationError({"ids": [f"The seat '{s}' does not exist."]})
|
||||||
|
else:
|
||||||
|
raise ValidationError("Please pass either seat_guids or ids.")
|
||||||
|
|
||||||
|
return {"seats": seats.values()}
|
||||||
|
|
||||||
|
|
||||||
class SeatSerializer(I18nAwareModelSerializer):
|
class SeatSerializer(I18nAwareModelSerializer):
|
||||||
orderposition = serializers.IntegerField(source='orderposition_id')
|
orderposition = serializers.IntegerField(source='orderposition_id')
|
||||||
cartposition = serializers.IntegerField(source='cartposition_id')
|
cartposition = serializers.IntegerField(source='cartposition_id')
|
||||||
|
|||||||
@@ -19,57 +19,8 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from django.conf import settings
|
|
||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
from i18nfield.fields import I18nCharField, I18nTextField
|
from i18nfield.rest_framework import I18nAwareModelSerializer, I18nField
|
||||||
from i18nfield.strings import LazyI18nString
|
|
||||||
from rest_framework.exceptions import ValidationError
|
|
||||||
from rest_framework.fields import Field
|
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class I18nField(Field):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self.allow_blank = kwargs.pop('allow_blank', False)
|
|
||||||
self.trim_whitespace = kwargs.pop('trim_whitespace', True)
|
|
||||||
self.max_length = kwargs.pop('max_length', None)
|
|
||||||
self.min_length = kwargs.pop('min_length', None)
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
def to_representation(self, value):
|
|
||||||
if hasattr(value, 'data'):
|
|
||||||
if isinstance(value.data, dict):
|
|
||||||
return value.data
|
|
||||||
elif value.data is None:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
settings.LANGUAGE_CODE: str(value.data)
|
|
||||||
}
|
|
||||||
elif value is None:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
settings.LANGUAGE_CODE: str(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
if isinstance(data, str):
|
|
||||||
return LazyI18nString(data)
|
|
||||||
elif isinstance(data, dict):
|
|
||||||
if any([k not in dict(settings.LANGUAGES) for k in data.keys()]):
|
|
||||||
raise ValidationError('Invalid languages included.')
|
|
||||||
return LazyI18nString(data)
|
|
||||||
else:
|
|
||||||
raise ValidationError('Invalid data type.')
|
|
||||||
|
|
||||||
|
|
||||||
class I18nAwareModelSerializer(ModelSerializer):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
I18nAwareModelSerializer.serializer_field_mapping[I18nCharField] = I18nField
|
|
||||||
I18nAwareModelSerializer.serializer_field_mapping[I18nTextField] = I18nField
|
|
||||||
|
|
||||||
|
|
||||||
class I18nURLField(I18nField):
|
class I18nURLField(I18nField):
|
||||||
@@ -84,3 +35,10 @@ class I18nURLField(I18nField):
|
|||||||
else:
|
else:
|
||||||
URLValidator()(value.data)
|
URLValidator()(value.data)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"I18nAwareModelSerializer", # for backwards compatibility
|
||||||
|
"I18nField", # for backwards compatibility
|
||||||
|
"I18nURLField",
|
||||||
|
]
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||||
RevokedTicketSecret,
|
PrintLog, RevokedTicketSecret,
|
||||||
)
|
)
|
||||||
from pretix.base.pdf import get_images, get_variables
|
from pretix.base.pdf import get_images, get_variables
|
||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
@@ -284,6 +284,26 @@ class CheckinSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
||||||
|
|
||||||
|
|
||||||
|
class PrintLogSerializer(serializers.ModelSerializer):
|
||||||
|
device_id = serializers.SlugRelatedField(
|
||||||
|
source='device',
|
||||||
|
slug_field='device_id',
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PrintLog
|
||||||
|
fields = (
|
||||||
|
"id",
|
||||||
|
"successful",
|
||||||
|
"datetime",
|
||||||
|
"source",
|
||||||
|
"type",
|
||||||
|
"device_id",
|
||||||
|
"info",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
||||||
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
|
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
|
||||||
raw_barcode = serializers.CharField(required=True, allow_null=False)
|
raw_barcode = serializers.CharField(required=True, allow_null=False)
|
||||||
@@ -476,6 +496,7 @@ class OrderPositionListSerializer(serializers.ListSerializer):
|
|||||||
|
|
||||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||||
checkins = CheckinSerializer(many=True, read_only=True)
|
checkins = CheckinSerializer(many=True, read_only=True)
|
||||||
|
print_logs = PrintLogSerializer(many=True, read_only=True)
|
||||||
answers = AnswerSerializer(many=True)
|
answers = AnswerSerializer(many=True)
|
||||||
downloads = PositionDownloadsField(source='*', read_only=True)
|
downloads = PositionDownloadsField(source='*', read_only=True)
|
||||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||||
@@ -490,12 +511,13 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
||||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||||
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
'print_logs', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id', 'pdf_data', 'seat',
|
||||||
|
'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
||||||
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
|
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id',
|
||||||
'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use'
|
'pdf_data', 'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -577,9 +599,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
|||||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
|
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat',
|
||||||
'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
|
'require_attention', 'order__status', 'order__valid_if_pending', 'order__require_approval',
|
||||||
'blocked')
|
'valid_from', 'valid_until', 'blocked')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -621,7 +643,8 @@ class OrderPaymentDateField(serializers.DateField):
|
|||||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderFee
|
model = OrderFee
|
||||||
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule',
|
||||||
|
'tax_code', 'canceled')
|
||||||
|
|
||||||
|
|
||||||
class PaymentURLField(serializers.URLField):
|
class PaymentURLField(serializers.URLField):
|
||||||
@@ -732,12 +755,12 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||||
'url', 'customer', 'valid_if_pending', 'api_meta'
|
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date'
|
||||||
)
|
)
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
||||||
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -1494,6 +1517,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
pos.answers = answers
|
pos.answers = answers
|
||||||
pos.pseudonymization_id = "PREVIEW"
|
pos.pseudonymization_id = "PREVIEW"
|
||||||
pos.checkins = []
|
pos.checkins = []
|
||||||
|
pos.print_logs = []
|
||||||
pos_map[pos.positionid] = pos
|
pos_map[pos.positionid] = pos
|
||||||
else:
|
else:
|
||||||
if pos.voucher:
|
if pos.voucher:
|
||||||
@@ -1654,7 +1678,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceLine
|
model = InvoiceLine
|
||||||
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
|
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
|
||||||
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name', 'fee_type',
|
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_code', 'tax_name', 'fee_type',
|
||||||
'fee_internal_type', 'event_location')
|
'fee_internal_type', 'event_location')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
||||||
from pretix.api.serializers import AsymmetricField
|
from pretix.api.serializers import AsymmetricField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import CompatibleJSONField
|
from pretix.api.serializers.order import CompatibleJSONField
|
||||||
@@ -297,6 +298,7 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
revoked = serializers.BooleanField(read_only=True)
|
revoked = serializers.BooleanField(read_only=True)
|
||||||
initialized = serializers.DateTimeField(read_only=True)
|
initialized = serializers.DateTimeField(read_only=True)
|
||||||
initialization_token = serializers.DateTimeField(read_only=True)
|
initialization_token = serializers.DateTimeField(read_only=True)
|
||||||
|
security_profile = serializers.ChoiceField(choices=[], required=False, default="full")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
@@ -306,6 +308,10 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
|
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['security_profile'].choices = [(k, v.verbose_name) for k, v in get_all_security_profiles().items()]
|
||||||
|
|
||||||
|
|
||||||
class TeamInviteSerializer(serializers.ModelSerializer):
|
class TeamInviteSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -32,10 +32,17 @@ from pretix.helpers.periodic import minimum_interval
|
|||||||
register_webhook_events = Signal()
|
register_webhook_events = Signal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known webhook events. Receivers should return an
|
This signal is sent out to get all known webhook events. Receivers should return an
|
||||||
instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such
|
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
|
||||||
instances.
|
instances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
register_device_security_profile = Signal()
|
||||||
|
"""
|
||||||
|
This signal is sent out to get all known device security_profiles. Receivers should
|
||||||
|
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
|
||||||
|
or a list of such instances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@receiver(periodic_task)
|
@receiver(periodic_task)
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ from pretix.base.models import (
|
|||||||
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
|
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
|
||||||
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
||||||
)
|
)
|
||||||
|
from pretix.base.models.orders import PrintLog
|
||||||
from pretix.base.services.checkin import (
|
from pretix.base.services.checkin import (
|
||||||
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||||
)
|
)
|
||||||
@@ -115,7 +116,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
if 'subevent' in self.request.query_params.getlist('expand'):
|
if 'subevent' in self.request.query_params.getlist('expand'):
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
||||||
'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
|
'subevent__seat_category_mappings', 'subevent__meta_values',
|
||||||
)
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -142,7 +143,9 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
|
instance.checkins.all().delete()
|
||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.checkinlist.deleted',
|
'pretix.event.checkinlist.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
@@ -365,8 +368,9 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
lookup='checkins',
|
lookup='checkins',
|
||||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
|
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||||
),
|
),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||||
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||||
@@ -378,6 +382,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
'positions',
|
'positions',
|
||||||
OrderPosition.objects.prefetch_related(
|
OrderPosition.objects.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -389,8 +394,9 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
lookup='checkins',
|
lookup='checkins',
|
||||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
|
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||||
),
|
),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import base64
|
import base64
|
||||||
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from cryptography.hazmat.backends.openssl.backend import Backend
|
from cryptography.hazmat.backends.openssl.backend import Backend
|
||||||
@@ -146,6 +147,8 @@ class InitializeView(APIView):
|
|||||||
permission_classes = ()
|
permission_classes = ()
|
||||||
|
|
||||||
def post(self, request, format=None):
|
def post(self, request, format=None):
|
||||||
|
from pretix.base.signals import device_info_updated
|
||||||
|
|
||||||
serializer = InitializationRequestSerializer(data=request.data)
|
serializer = InitializationRequestSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
@@ -160,6 +163,8 @@ class InitializeView(APIView):
|
|||||||
if device.revoked:
|
if device.revoked:
|
||||||
raise ValidationError({'token': ['This initialization token has been revoked.']})
|
raise ValidationError({'token': ['This initialization token has been revoked.']})
|
||||||
|
|
||||||
|
old_instance = copy.copy(device)
|
||||||
|
|
||||||
device.initialized = now()
|
device.initialized = now()
|
||||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
@@ -174,6 +179,10 @@ class InitializeView(APIView):
|
|||||||
|
|
||||||
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
|
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
|
||||||
|
|
||||||
|
device_info_updated.send(
|
||||||
|
sender=Device, old_device=old_instance, new_device=device
|
||||||
|
)
|
||||||
|
|
||||||
serializer = DeviceSerializer(device)
|
serializer = DeviceSerializer(device)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@@ -182,9 +191,12 @@ class UpdateView(APIView):
|
|||||||
authentication_classes = (DeviceTokenAuthentication,)
|
authentication_classes = (DeviceTokenAuthentication,)
|
||||||
|
|
||||||
def post(self, request, format=None):
|
def post(self, request, format=None):
|
||||||
|
from pretix.base.signals import device_info_updated
|
||||||
|
|
||||||
serializer = UpdateRequestSerializer(data=request.data)
|
serializer = UpdateRequestSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
device = request.auth
|
device = request.auth
|
||||||
|
old_instance = copy.copy(device)
|
||||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
device.os_name = serializer.validated_data.get('os_name')
|
device.os_name = serializer.validated_data.get('os_name')
|
||||||
@@ -200,9 +212,8 @@ class UpdateView(APIView):
|
|||||||
device.save()
|
device.save()
|
||||||
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
||||||
|
|
||||||
from ...base.signals import device_info_updated
|
|
||||||
device_info_updated.send(
|
device_info_updated.send(
|
||||||
sender=Device, old_device=request.auth, new_device=device
|
sender=Device, old_device=old_instance, new_device=device
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = DeviceSerializer(device)
|
serializer = DeviceSerializer(device)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from django.utils.timezone import now
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import serializers, views, viewsets
|
from rest_framework import serializers, views, viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import (
|
from rest_framework.exceptions import (
|
||||||
NotFound, PermissionDenied, ValidationError,
|
NotFound, PermissionDenied, ValidationError,
|
||||||
)
|
)
|
||||||
@@ -50,8 +51,9 @@ from pretix.api.auth.permission import EventCRUDPermission
|
|||||||
from pretix.api.pagination import TotalOrderingFilter
|
from pretix.api.pagination import TotalOrderingFilter
|
||||||
from pretix.api.serializers.event import (
|
from pretix.api.serializers.event import (
|
||||||
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
||||||
EventSettingsSerializer, ItemMetaPropertiesSerializer, SeatSerializer,
|
EventSettingsSerializer, ItemMetaPropertiesSerializer,
|
||||||
SubEventSerializer, TaxRuleSerializer,
|
SeatBulkBlockInputSerializer, SeatSerializer, SubEventSerializer,
|
||||||
|
TaxRuleSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.views import ConditionalListView
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -237,9 +239,9 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
|
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
|
||||||
changed = merge_dicts(enabled, disabled)
|
changed = merge_dicts(enabled, disabled)
|
||||||
|
|
||||||
for module, action in changed.items():
|
for module, operation in changed.items():
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.plugins.' + action,
|
'pretix.event.plugins.' + operation,
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
data={'plugin': module}
|
data={'plugin': module}
|
||||||
@@ -744,3 +746,24 @@ class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
data={"seats": [serializer.instance.pk]},
|
data={"seats": [serializer.instance.pk]},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def bulk_change_blocked(self, blocked):
|
||||||
|
s = SeatBulkBlockInputSerializer(
|
||||||
|
data=self.request.data,
|
||||||
|
context={"event": self.request.event, "queryset": self.get_queryset()},
|
||||||
|
)
|
||||||
|
s.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
seats = s.validated_data["seats"]
|
||||||
|
for seat in seats:
|
||||||
|
seat.blocked = blocked
|
||||||
|
Seat.objects.bulk_update(seats, ["blocked"], batch_size=1000)
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
@action(methods=["POST"], detail=False)
|
||||||
|
def bulk_block(self, request, *args, **kwargs):
|
||||||
|
return self.bulk_change_blocked(True)
|
||||||
|
|
||||||
|
@action(methods=["POST"], detail=False)
|
||||||
|
def bulk_unblock(self, request, *args, **kwargs):
|
||||||
|
return self.bulk_change_blocked(False)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from pretix.base.models import (
|
|||||||
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
|
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
|
||||||
ReusableMedium,
|
ReusableMedium,
|
||||||
)
|
)
|
||||||
|
from pretix.base.models.orders import PrintLog
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
|||||||
'order', 'order__event', 'order__event__organizer', 'seat',
|
'order', 'order__event', 'order__event__organizer', 'seat',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from django.db.models import (
|
|||||||
from django.db.models.functions import Coalesce, Concat
|
from django.db.models.functions import Coalesce, Concat
|
||||||
from django.http import FileResponse, HttpResponse
|
from django.http import FileResponse, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils import formats
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
@@ -57,7 +58,8 @@ from pretix.api.serializers.order import (
|
|||||||
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
||||||
OrderPositionSerializer, OrderRefundCreateSerializer,
|
OrderPositionSerializer, OrderRefundCreateSerializer,
|
||||||
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
||||||
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
|
PrintLogSerializer, RevokedTicketSecretSerializer,
|
||||||
|
SimulatedOrderSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.orderchange import (
|
from pretix.api.serializers.orderchange import (
|
||||||
BlockNameSerializer, OrderChangeOperationSerializer,
|
BlockNameSerializer, OrderChangeOperationSerializer,
|
||||||
@@ -66,6 +68,7 @@ from pretix.api.serializers.orderchange import (
|
|||||||
OrderPositionInfoPatchSerializer,
|
OrderPositionInfoPatchSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.views import RichOrderingFilter
|
from pretix.api.views import RichOrderingFilter
|
||||||
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
|
CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue,
|
||||||
@@ -75,7 +78,7 @@ from pretix.base.models import (
|
|||||||
TeamAPIToken, generate_secret,
|
TeamAPIToken, generate_secret,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret,
|
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret,
|
||||||
)
|
)
|
||||||
from pretix.base.payment import PaymentException
|
from pretix.base.payment import PaymentException
|
||||||
from pretix.base.pdf import get_images
|
from pretix.base.pdf import get_images
|
||||||
@@ -96,7 +99,6 @@ from pretix.base.services.tickets import generate
|
|||||||
from pretix.base.signals import (
|
from pretix.base.signals import (
|
||||||
order_modified, order_paid, order_placed, register_ticket_outputs,
|
order_modified, order_paid, order_placed, register_ticket_outputs,
|
||||||
)
|
)
|
||||||
from pretix.base.templatetags.money import money_filter
|
|
||||||
from pretix.control.signals import order_search_filter_q
|
from pretix.control.signals import order_search_filter_q
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
|
|
||||||
@@ -214,7 +216,7 @@ class OrderViewSetMixin:
|
|||||||
queryset = Order.objects.none()
|
queryset = Order.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||||
ordering = ('datetime',)
|
ordering = ('datetime',)
|
||||||
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
|
ordering_fields = ('datetime', 'code', 'status', 'last_modified', 'cancellation_date')
|
||||||
filterset_class = OrderFilter
|
filterset_class = OrderFilter
|
||||||
lookup_field = 'code'
|
lookup_field = 'code'
|
||||||
|
|
||||||
@@ -259,6 +261,7 @@ class OrderViewSetMixin:
|
|||||||
'positions',
|
'positions',
|
||||||
opq.all().prefetch_related(
|
opq.all().prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
|
||||||
)),
|
)),
|
||||||
@@ -280,6 +283,7 @@ class OrderViewSetMixin:
|
|||||||
'positions',
|
'positions',
|
||||||
opq.all().prefetch_related(
|
opq.all().prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'item', 'variation',
|
'item', 'variation',
|
||||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
||||||
'seat',
|
'seat',
|
||||||
@@ -643,6 +647,8 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
order = self.get_object()
|
order = self.get_object()
|
||||||
order.secret = generate_secret()
|
order.secret = generate_secret()
|
||||||
for op in order.all_positions.all():
|
for op in order.all_positions.all():
|
||||||
|
op.web_secret = generate_secret()
|
||||||
|
op.save(update_fields=["web_secret"])
|
||||||
assign_ticket_secret(
|
assign_ticket_secret(
|
||||||
request.event, op, force_invalidate=True, save=True
|
request.event, op, force_invalidate=True, save=True
|
||||||
)
|
)
|
||||||
@@ -1093,6 +1099,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||||
to_attr='meta_values_cached')
|
to_attr='meta_values_cached')
|
||||||
@@ -1136,6 +1143,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
else:
|
else:
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
).select_related(
|
).select_related(
|
||||||
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
||||||
@@ -1223,9 +1231,10 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
price = get_price(**kwargs)
|
price = get_price(**kwargs)
|
||||||
tr = kwargs.get('tax_rule', kwargs.get('item').tax_rule)
|
tr = kwargs.get('tax_rule', kwargs.get('item').tax_rule)
|
||||||
with language(data.get('locale') or self.request.event.settings.locale, self.request.event.settings.region):
|
with language(data.get('locale') or self.request.event.settings.locale, self.request.event.settings.region):
|
||||||
|
gross_formatted = formats.localize_input(round_decimal(price.gross, self.request.event.currency))
|
||||||
return Response({
|
return Response({
|
||||||
'gross': price.gross,
|
'gross': price.gross,
|
||||||
'gross_formatted': money_filter(price.gross, self.request.event.currency, hide_currency=True),
|
'gross_formatted': gross_formatted,
|
||||||
'net': price.net,
|
'net': price.net,
|
||||||
'rate': price.rate,
|
'rate': price.rate,
|
||||||
'name': str(price.name),
|
'name': str(price.name),
|
||||||
@@ -1254,6 +1263,34 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
|
||||||
|
def printlog(self, request, **kwargs):
|
||||||
|
pos = self.get_object()
|
||||||
|
serializer = PrintLogSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
serializer.save(
|
||||||
|
position=pos,
|
||||||
|
device=request.auth if isinstance(request.auth, Device) else None,
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
|
||||||
|
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
pos.order.log_action(
|
||||||
|
"pretix.event.order.print",
|
||||||
|
data={
|
||||||
|
"position": pos.pk,
|
||||||
|
"positionid": pos.positionid,
|
||||||
|
**serializer.validated_data,
|
||||||
|
},
|
||||||
|
auth=request.auth,
|
||||||
|
user=request.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
@action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)')
|
@action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)')
|
||||||
def pdf_image(self, request, key, **kwargs):
|
def pdf_image(self, request, key, **kwargs):
|
||||||
pos = self.get_object()
|
pos = self.get_object()
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class NativeAuthBackend(BaseAuthBackend):
|
|||||||
to log in.
|
to log in.
|
||||||
"""
|
"""
|
||||||
d = OrderedDict([
|
d = OrderedDict([
|
||||||
('email', forms.EmailField(label=_("E-mail"), max_length=254,
|
('email', forms.EmailField(label=_("Email"), max_length=254,
|
||||||
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
|
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
|
||||||
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput,
|
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput,
|
||||||
max_length=4096)),
|
max_length=4096)),
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from django.utils.translation import get_language, gettext_lazy as _
|
|||||||
from pretix.base.models import Event
|
from pretix.base.models import Event
|
||||||
from pretix.base.signals import register_html_mail_renderers
|
from pretix.base.signals import register_html_mail_renderers
|
||||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||||
|
from pretix.helpers.format import SafeFormatter, format_map
|
||||||
|
|
||||||
from pretix.base.services.placeholders import ( # noqa
|
from pretix.base.services.placeholders import ( # noqa
|
||||||
get_available_placeholders, PlaceholderContext
|
get_available_placeholders, PlaceholderContext
|
||||||
@@ -68,7 +69,7 @@ def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
|||||||
|
|
||||||
class BaseHTMLMailRenderer:
|
class BaseHTMLMailRenderer:
|
||||||
"""
|
"""
|
||||||
This is the base class for all HTML e-mail renderers.
|
This is the base class for all HTML email renderers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, event: Event, organizer=None):
|
def __init__(self, event: Event, organizer=None):
|
||||||
@@ -79,7 +80,7 @@ class BaseHTMLMailRenderer:
|
|||||||
return self.identifier
|
return self.identifier
|
||||||
|
|
||||||
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
|
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
|
||||||
position=None) -> str:
|
position=None, context=None) -> str:
|
||||||
"""
|
"""
|
||||||
This method should generate the HTML part of the email.
|
This method should generate the HTML part of the email.
|
||||||
|
|
||||||
@@ -88,6 +89,7 @@ class BaseHTMLMailRenderer:
|
|||||||
:param subject: The email subject.
|
:param subject: The email subject.
|
||||||
:param order: The order if this email is connected to one, otherwise ``None``.
|
:param order: The order if this email is connected to one, otherwise ``None``.
|
||||||
:param position: The order position if this email is connected to one, otherwise ``None``.
|
:param position: The order position if this email is connected to one, otherwise ``None``.
|
||||||
|
:param context: Context to use to render placeholders in the plain body
|
||||||
:return: An HTML string
|
:return: An HTML string
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@@ -134,8 +136,10 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
|||||||
def compile_markdown(self, plaintext):
|
def compile_markdown(self, plaintext):
|
||||||
return markdown_compile_email(plaintext)
|
return markdown_compile_email(plaintext)
|
||||||
|
|
||||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
|
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
|
||||||
body_md = self.compile_markdown(plain_body)
|
body_md = self.compile_markdown(plain_body)
|
||||||
|
if context:
|
||||||
|
body_md = format_map(body_md, context=context, mode=SafeFormatter.MODE_RICH_TO_HTML)
|
||||||
htmlctx = {
|
htmlctx = {
|
||||||
'site': settings.PRETIX_INSTANCE_NAME,
|
'site': settings.PRETIX_INSTANCE_NAME,
|
||||||
'site_url': settings.SITE_URL,
|
'site_url': settings.SITE_URL,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
|
|||||||
_('Customer ID'),
|
_('Customer ID'),
|
||||||
_('SSO provider'),
|
_('SSO provider'),
|
||||||
_('External identifier'),
|
_('External identifier'),
|
||||||
_('E-mail'),
|
_('Email'),
|
||||||
_('Phone number'),
|
_('Phone number'),
|
||||||
_('Full name'),
|
_('Full name'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
|||||||
_('Invoice number'),
|
_('Invoice number'),
|
||||||
_('Date'),
|
_('Date'),
|
||||||
_('Order code'),
|
_('Order code'),
|
||||||
_('E-mail address'),
|
_('Email address'),
|
||||||
_('Invoice type'),
|
_('Invoice type'),
|
||||||
_('Cancellation of'),
|
_('Cancellation of'),
|
||||||
_('Language'),
|
_('Language'),
|
||||||
@@ -326,7 +326,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
|||||||
_('Event start date'),
|
_('Event start date'),
|
||||||
_('Date'),
|
_('Date'),
|
||||||
_('Order code'),
|
_('Order code'),
|
||||||
_('E-mail address'),
|
_('Email address'),
|
||||||
_('Invoice type'),
|
_('Invoice type'),
|
||||||
_('Cancellation of'),
|
_('Cancellation of'),
|
||||||
_('Invoice sender:') + ' ' + _('Name'),
|
_('Invoice sender:') + ' ' + _('Name'),
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
headers.append(_('Comment'))
|
headers.append(_('Comment'))
|
||||||
headers.append(_('Follow-up date'))
|
headers.append(_('Follow-up date'))
|
||||||
headers.append(_('Positions'))
|
headers.append(_('Positions'))
|
||||||
headers.append(_('E-mail address verified'))
|
headers.append(_('Email address verified'))
|
||||||
headers.append(_('External customer ID'))
|
headers.append(_('External customer ID'))
|
||||||
headers.append(_('Payment providers'))
|
headers.append(_('Payment providers'))
|
||||||
if form_data.get('include_payment_amounts'):
|
if form_data.get('include_payment_amounts'):
|
||||||
@@ -655,7 +655,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
headers += [
|
headers += [
|
||||||
_('Sales channel'),
|
_('Sales channel'),
|
||||||
_('Order locale'),
|
_('Order locale'),
|
||||||
_('E-mail address verified'),
|
_('Email address verified'),
|
||||||
_('External customer ID'),
|
_('External customer ID'),
|
||||||
_('Check-in lists'),
|
_('Check-in lists'),
|
||||||
_('Payment providers'),
|
_('Payment providers'),
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class MarkdownTextarea(forms.Textarea):
|
|||||||
|
|
||||||
|
|
||||||
class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
|
class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
|
||||||
def format_output(self, rendered_widgets) -> str:
|
def format_output(self, rendered_widgets, id_) -> str:
|
||||||
rendered_widgets = rendered_widgets + [
|
rendered_widgets = rendered_widgets + [
|
||||||
'<div class="i18n-field-markdown-note">%s</div>' % (
|
'<div class="i18n-field-markdown-note">%s</div>' % (
|
||||||
_("You can use {markup_name} in this field.").format(
|
_("You can use {markup_name} in this field.").format(
|
||||||
@@ -108,11 +108,11 @@ class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
return super().format_output(rendered_widgets)
|
return super().format_output(rendered_widgets, id_)
|
||||||
|
|
||||||
|
|
||||||
class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
|
class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
|
||||||
def format_output(self, rendered_widgets) -> str:
|
def format_output(self, rendered_widgets, id_) -> str:
|
||||||
rendered_widgets = rendered_widgets + [
|
rendered_widgets = rendered_widgets + [
|
||||||
'<div class="i18n-field-markdown-note">%s</div>' % (
|
'<div class="i18n-field-markdown-note">%s</div>' % (
|
||||||
_("You can use {markup_name} in this field.").format(
|
_("You can use {markup_name} in this field.").format(
|
||||||
@@ -120,7 +120,7 @@ class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
return super().format_output(rendered_widgets)
|
return super().format_output(rendered_widgets, id_)
|
||||||
|
|
||||||
|
|
||||||
SECRET_REDACTED = '*****'
|
SECRET_REDACTED = '*****'
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ class PasswordRecoverForm(forms.Form):
|
|||||||
|
|
||||||
class PasswordForgotForm(forms.Form):
|
class PasswordForgotForm(forms.Form):
|
||||||
email = forms.EmailField(
|
email = forms.EmailField(
|
||||||
label=_('E-mail'),
|
label=_('Email'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ from django.core.validators import (
|
|||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.forms import Select, widgets
|
from django.forms import Select, widgets
|
||||||
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
@@ -77,7 +78,7 @@ from pretix.base.i18n import (
|
|||||||
get_babel_locale, get_language_without_region, language,
|
get_babel_locale, get_language_without_region, language,
|
||||||
)
|
)
|
||||||
from pretix.base.models import InvoiceAddress, Item, Question, QuestionOption
|
from pretix.base.models import InvoiceAddress, Item, Question, QuestionOption
|
||||||
from pretix.base.models.tax import VAT_ID_COUNTRIES, ask_for_vat_id
|
from pretix.base.models.tax import ask_for_vat_id
|
||||||
from pretix.base.services.tax import (
|
from pretix.base.services.tax import (
|
||||||
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
|
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
|
||||||
)
|
)
|
||||||
@@ -276,6 +277,10 @@ class NamePartsFormField(forms.MultiValueField):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def name_parts_is_empty(name_parts_dict):
|
||||||
|
return not any(k != "_scheme" and v for k, v in name_parts_dict.items())
|
||||||
|
|
||||||
|
|
||||||
class WrappedPhonePrefixSelect(Select):
|
class WrappedPhonePrefixSelect(Select):
|
||||||
initial = None
|
initial = None
|
||||||
|
|
||||||
@@ -602,6 +607,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
questions = pos.item.questions_to_ask
|
questions = pos.item.questions_to_ask
|
||||||
event = kwargs.pop('event')
|
event = kwargs.pop('event')
|
||||||
self.all_optional = kwargs.pop('all_optional', False)
|
self.all_optional = kwargs.pop('all_optional', False)
|
||||||
|
self.attendee_addresses_required = event.settings.attendee_addresses_required and not self.all_optional
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@@ -676,7 +682,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
|
|
||||||
if item.ask_attendee_data and event.settings.attendee_addresses_asked:
|
if item.ask_attendee_data and event.settings.attendee_addresses_asked:
|
||||||
add_fields['street'] = forms.CharField(
|
add_fields['street'] = forms.CharField(
|
||||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
required=self.attendee_addresses_required,
|
||||||
label=_('Address'),
|
label=_('Address'),
|
||||||
widget=forms.Textarea(attrs={
|
widget=forms.Textarea(attrs={
|
||||||
'rows': 2,
|
'rows': 2,
|
||||||
@@ -686,7 +692,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
initial=(cartpos.street if cartpos else orderpos.street),
|
initial=(cartpos.street if cartpos else orderpos.street),
|
||||||
)
|
)
|
||||||
add_fields['zipcode'] = forms.CharField(
|
add_fields['zipcode'] = forms.CharField(
|
||||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
required=False,
|
||||||
max_length=30,
|
max_length=30,
|
||||||
label=_('ZIP code'),
|
label=_('ZIP code'),
|
||||||
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
|
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
|
||||||
@@ -695,7 +701,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
add_fields['city'] = forms.CharField(
|
add_fields['city'] = forms.CharField(
|
||||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
required=False,
|
||||||
label=_('City'),
|
label=_('City'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
initial=(cartpos.city if cartpos else orderpos.city),
|
initial=(cartpos.city if cartpos else orderpos.city),
|
||||||
@@ -707,11 +713,12 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
add_fields['country'] = CountryField(
|
add_fields['country'] = CountryField(
|
||||||
countries=CachedCountries
|
countries=CachedCountries
|
||||||
).formfield(
|
).formfield(
|
||||||
required=event.settings.attendee_addresses_required and not self.all_optional,
|
required=self.attendee_addresses_required,
|
||||||
label=_('Country'),
|
label=_('Country'),
|
||||||
initial=country,
|
initial=country,
|
||||||
widget=forms.Select(attrs={
|
widget=forms.Select(attrs={
|
||||||
'autocomplete': 'country',
|
'autocomplete': 'country',
|
||||||
|
'data-country-information-url': reverse('js_helpers.states'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||||
@@ -946,9 +953,9 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
d = super().clean()
|
d = super().clean()
|
||||||
|
|
||||||
if self.address_validation:
|
if self.address_validation:
|
||||||
self.cleaned_data = d = validate_address(d, True)
|
self.cleaned_data = d = validate_address(d, all_optional=not self.attendee_addresses_required)
|
||||||
|
|
||||||
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
if d.get('street') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
if not d.get('state'):
|
if not d.get('state'):
|
||||||
self.add_error('state', _('This field is required.'))
|
self.add_error('state', _('This field is required.'))
|
||||||
|
|
||||||
@@ -1005,7 +1012,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
'street': forms.Textarea(attrs={
|
'street': forms.Textarea(attrs={
|
||||||
'rows': 2,
|
'rows': 2,
|
||||||
'placeholder': _('Street and Number'),
|
'placeholder': _('Street and Number'),
|
||||||
'autocomplete': 'street-address'
|
'autocomplete': 'street-address',
|
||||||
}),
|
}),
|
||||||
'beneficiary': forms.Textarea(attrs={'rows': 3}),
|
'beneficiary': forms.Textarea(attrs={'rows': 3}),
|
||||||
'country': forms.Select(attrs={
|
'country': forms.Select(attrs={
|
||||||
@@ -1021,13 +1028,25 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
'data-display-dependency': '#id_is_business_1',
|
'data-display-dependency': '#id_is_business_1',
|
||||||
'autocomplete': 'organization',
|
'autocomplete': 'organization',
|
||||||
}),
|
}),
|
||||||
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-with-vat-id': ','.join(VAT_ID_COUNTRIES)}),
|
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
|
||||||
'internal_reference': forms.TextInput,
|
'internal_reference': forms.TextInput,
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'is_business': ''
|
'is_business': ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ask_vat_id(self):
|
||||||
|
return self.event.settings.invoice_address_vatid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def address_required(self):
|
||||||
|
return self.event.settings.invoice_address_required
|
||||||
|
|
||||||
|
@property
|
||||||
|
def company_required(self):
|
||||||
|
return self.event.settings.invoice_address_company_required
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.event = event = kwargs.pop('event')
|
self.event = event = kwargs.pop('event')
|
||||||
self.request = kwargs.pop('request', None)
|
self.request = kwargs.pop('request', None)
|
||||||
@@ -1039,7 +1058,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
|
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if not event.settings.invoice_address_vatid:
|
|
||||||
|
self.fields["company"].widget.attrs["data-display-dependency"] = f'#id_{self.add_prefix("is_business")}_1'
|
||||||
|
self.fields["vat_id"].widget.attrs["data-display-dependency"] = f'#id_{self.add_prefix("is_business")}_1'
|
||||||
|
|
||||||
|
if not self.ask_vat_id:
|
||||||
del self.fields['vat_id']
|
del self.fields['vat_id']
|
||||||
elif self.validate_vat_id:
|
elif self.validate_vat_id:
|
||||||
self.fields['vat_id'].help_text = '<br/>'.join([
|
self.fields['vat_id'].help_text = '<br/>'.join([
|
||||||
@@ -1055,6 +1078,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
])
|
])
|
||||||
|
|
||||||
self.fields['country'].choices = CachedCountries()
|
self.fields['country'].choices = CachedCountries()
|
||||||
|
self.fields['country'].widget.attrs['data-country-information-url'] = reverse('js_helpers.states')
|
||||||
|
|
||||||
c = [('', pgettext_lazy('address', 'Select state'))]
|
c = [('', pgettext_lazy('address', 'Select state'))]
|
||||||
fprefix = self.prefix + '-' if self.prefix else ''
|
fprefix = self.prefix + '-' if self.prefix else ''
|
||||||
@@ -1083,18 +1107,22 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
)
|
)
|
||||||
self.fields['state'].widget.is_required = True
|
self.fields['state'].widget.is_required = True
|
||||||
|
|
||||||
|
self.fields['street'].required = False
|
||||||
|
self.fields['zipcode'].required = False
|
||||||
|
self.fields['city'].required = False
|
||||||
|
|
||||||
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
|
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
|
||||||
if cc and not ask_for_vat_id(cc) and fprefix + 'vat_id' in self.data:
|
if cc and not ask_for_vat_id(cc) and fprefix + 'vat_id' in self.data:
|
||||||
self.data = self.data.copy()
|
self.data = self.data.copy()
|
||||||
del self.data[fprefix + 'vat_id']
|
del self.data[fprefix + 'vat_id']
|
||||||
|
|
||||||
if not event.settings.invoice_address_required or self.all_optional:
|
if not self.address_required or self.all_optional:
|
||||||
for k, f in self.fields.items():
|
for k, f in self.fields.items():
|
||||||
f.required = False
|
f.required = False
|
||||||
f.widget.is_required = False
|
f.widget.is_required = False
|
||||||
if 'required' in f.widget.attrs:
|
if 'required' in f.widget.attrs:
|
||||||
del f.widget.attrs['required']
|
del f.widget.attrs['required']
|
||||||
elif event.settings.invoice_address_company_required and not self.all_optional:
|
elif self.company_required and not self.all_optional:
|
||||||
self.initial['is_business'] = True
|
self.initial['is_business'] = True
|
||||||
|
|
||||||
self.fields['is_business'].widget = BusinessBooleanRadio(require_business=True)
|
self.fields['is_business'].widget = BusinessBooleanRadio(require_business=True)
|
||||||
@@ -1111,11 +1139,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
label=_('Name'),
|
label=_('Name'),
|
||||||
initial=self.instance.name_parts,
|
initial=self.instance.name_parts,
|
||||||
)
|
)
|
||||||
if event.settings.invoice_address_required and not event.settings.invoice_address_company_required and not self.all_optional:
|
if self.address_required and not self.company_required and not self.all_optional:
|
||||||
if not event.settings.invoice_name_required:
|
if not event.settings.invoice_name_required:
|
||||||
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0'
|
self.fields['name_parts'].widget.attrs['data-required-if'] = f'#id_{self.add_prefix("is_business")}_0'
|
||||||
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
|
self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1'
|
||||||
self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1'
|
self.fields['company'].widget.attrs['data-required-if'] = f'#id_{self.add_prefix("is_business")}_1'
|
||||||
|
|
||||||
if not event.settings.invoice_address_beneficiary:
|
if not event.settings.invoice_address_beneficiary:
|
||||||
del self.fields['beneficiary']
|
del self.fields['beneficiary']
|
||||||
@@ -1135,16 +1163,19 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
validate_address # local import to prevent impact on startup time
|
validate_address # local import to prevent impact on startup time
|
||||||
|
|
||||||
data = self.cleaned_data
|
data = self.cleaned_data
|
||||||
|
|
||||||
if not data.get('is_business'):
|
if not data.get('is_business'):
|
||||||
data['company'] = ''
|
data['company'] = ''
|
||||||
data['vat_id'] = ''
|
data['vat_id'] = ''
|
||||||
if data.get('is_business') and not ask_for_vat_id(data.get('country')):
|
if data.get('is_business') and not ask_for_vat_id(data.get('country')):
|
||||||
data['vat_id'] = ''
|
data['vat_id'] = ''
|
||||||
if self.event.settings.invoice_address_required:
|
if self.address_validation and self.address_required and not self.all_optional:
|
||||||
if data.get('is_business') and not data.get('company'):
|
if data.get('is_business') and not data.get('company'):
|
||||||
raise ValidationError(_('You need to provide a company name.'))
|
raise ValidationError({"company": _('You need to provide a company name.')})
|
||||||
if not data.get('is_business') and not data.get('name_parts'):
|
if not data.get('is_business') and name_parts_is_empty(data.get('name_parts', {})):
|
||||||
raise ValidationError(_('You need to provide your name.'))
|
raise ValidationError(_('You need to provide your name.'))
|
||||||
|
if not data.get('street') and not data.get('zipcode') and not data.get('city'):
|
||||||
|
raise ValidationError({"street": _('This field is required.')})
|
||||||
|
|
||||||
if 'vat_id' in self.changed_data or not data.get('vat_id'):
|
if 'vat_id' in self.changed_data or not data.get('vat_id'):
|
||||||
self.instance.vat_id_validated = False
|
self.instance.vat_id_validated = False
|
||||||
@@ -1156,7 +1187,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
|
|
||||||
if all(
|
if all(
|
||||||
not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts')
|
not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts')
|
||||||
) and len(data.get('name_parts', {})) == 1:
|
) and name_parts_is_empty(data.get('name_parts', {})):
|
||||||
# Do not save the country if it is the only field set -- we don't know the user even checked it!
|
# Do not save the country if it is the only field set -- we don't know the user even checked it!
|
||||||
self.cleaned_data['country'] = ''
|
self.cleaned_data['country'] = ''
|
||||||
|
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ from pretix.control.forms import SingleLanguageWidget
|
|||||||
|
|
||||||
class UserSettingsForm(forms.ModelForm):
|
class UserSettingsForm(forms.ModelForm):
|
||||||
error_messages = {
|
error_messages = {
|
||||||
'duplicate_identifier': _("There already is an account associated with this e-mail address. "
|
'duplicate_identifier': _("There already is an account associated with this email address. "
|
||||||
"Please choose a different one."),
|
"Please choose a different one."),
|
||||||
'pw_current': _("Please enter your current password if you want to change your e-mail "
|
'pw_current': _("Please enter your current password if you want to change your email address "
|
||||||
"address or password."),
|
"or password."),
|
||||||
'pw_current_wrong': _("The current password you entered was not correct."),
|
'pw_current_wrong': _("The current password you entered was not correct."),
|
||||||
'pw_mismatch': _("Please enter the same password twice"),
|
'pw_mismatch': _("Please enter the same password twice"),
|
||||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
def _clean_text(self, text, tags=None):
|
def _clean_text(self, text, tags=None):
|
||||||
return self._normalize(bleach.clean(
|
return self._normalize(bleach.clean(
|
||||||
text,
|
text,
|
||||||
tags=tags or []
|
tags=set(tags) if tags else set()
|
||||||
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
||||||
|
|
||||||
|
|
||||||
@@ -461,7 +461,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
def _draw_event(self, canvas):
|
def _draw_event(self, canvas):
|
||||||
def shorten(txt):
|
def shorten(txt):
|
||||||
txt = str(txt)
|
txt = str(txt)
|
||||||
txt = bleach.clean(txt, tags=[]).strip()
|
txt = bleach.clean(txt, tags=set()).strip()
|
||||||
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||||
p_size = p.wrap(self.event_width, self.event_height)
|
p_size = p.wrap(self.event_width, self.event_height)
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class Command(BaseCommand):
|
|||||||
try:
|
try:
|
||||||
r = receiver(signal=periodic_task, sender=self)
|
r = receiver(signal=periodic_task, sender=self)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
if isinstance(Exception, KeyboardInterrupt):
|
if isinstance(err, KeyboardInterrupt):
|
||||||
raise err
|
raise err
|
||||||
if settings.SENTRY_ENABLED:
|
if settings.SENTRY_ENABLED:
|
||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ class BaseMediaType:
|
|||||||
def verbose_name(self):
|
def verbose_name(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""
|
||||||
|
This can be:
|
||||||
|
|
||||||
|
- The name of a Font Awesome icon to represent this channel type.
|
||||||
|
- The name of a SVG icon file that is resolvable through the static file system. We recommend to design for a size of 18x14 pixels.
|
||||||
|
"""
|
||||||
|
return "circle"
|
||||||
|
|
||||||
def generate_identifier(self, organizer):
|
def generate_identifier(self, organizer):
|
||||||
if self.medium_created_by_server:
|
if self.medium_created_by_server:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@@ -59,6 +69,7 @@ class BaseMediaType:
|
|||||||
class BarcodePlainMediaType(BaseMediaType):
|
class BarcodePlainMediaType(BaseMediaType):
|
||||||
identifier = 'barcode'
|
identifier = 'barcode'
|
||||||
verbose_name = _('Barcode / QR-Code')
|
verbose_name = _('Barcode / QR-Code')
|
||||||
|
icon = 'qrcode'
|
||||||
medium_created_by_server = True
|
medium_created_by_server = True
|
||||||
supports_giftcard = False
|
supports_giftcard = False
|
||||||
supports_orderposition = True
|
supports_orderposition = True
|
||||||
@@ -75,6 +86,7 @@ class BarcodePlainMediaType(BaseMediaType):
|
|||||||
class NfcUidMediaType(BaseMediaType):
|
class NfcUidMediaType(BaseMediaType):
|
||||||
identifier = 'nfc_uid'
|
identifier = 'nfc_uid'
|
||||||
verbose_name = _('NFC UID-based')
|
verbose_name = _('NFC UID-based')
|
||||||
|
icon = 'pretixbase/img/media/nfc_uid.svg'
|
||||||
medium_created_by_server = False
|
medium_created_by_server = False
|
||||||
supports_giftcard = True
|
supports_giftcard = True
|
||||||
supports_orderposition = False
|
supports_orderposition = False
|
||||||
@@ -114,6 +126,7 @@ class NfcUidMediaType(BaseMediaType):
|
|||||||
class NfcMf0aesMediaType(BaseMediaType):
|
class NfcMf0aesMediaType(BaseMediaType):
|
||||||
identifier = 'nfc_mf0aes'
|
identifier = 'nfc_mf0aes'
|
||||||
verbose_name = 'NFC Mifare Ultralight AES'
|
verbose_name = 'NFC Mifare Ultralight AES'
|
||||||
|
icon = 'pretixbase/img/media/nfc_secure.svg'
|
||||||
medium_created_by_server = False
|
medium_created_by_server = False
|
||||||
supports_giftcard = True
|
supports_giftcard = True
|
||||||
supports_orderposition = False
|
supports_orderposition = False
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Migration(migrations.Migration):
|
|||||||
('password', models.CharField(verbose_name='password', max_length=128)),
|
('password', models.CharField(verbose_name='password', max_length=128)),
|
||||||
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)),
|
('last_login', models.DateTimeField(verbose_name='last login', blank=True, null=True)),
|
||||||
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
|
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
|
||||||
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='E-mail', null=True,
|
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='Email', null=True,
|
||||||
db_index=True)),
|
db_index=True)),
|
||||||
('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)),
|
('givenname', models.CharField(verbose_name='Given name', max_length=255, blank=True, null=True)),
|
||||||
('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),
|
('familyname', models.CharField(verbose_name='Family name', max_length=255, blank=True, null=True)),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from decimal import Decimal
|
|||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import i18nfield.fields
|
import i18nfield.fields
|
||||||
|
from argon2.exceptions import HashingError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@@ -25,7 +26,14 @@ def initial_user(apps, schema_editor):
|
|||||||
user = User(email='admin@localhost')
|
user = User(email='admin@localhost')
|
||||||
user.is_staff = True
|
user.is_staff = True
|
||||||
user.is_superuser = True
|
user.is_superuser = True
|
||||||
user.password = make_password('admin')
|
try:
|
||||||
|
user.password = make_password('admin')
|
||||||
|
except HashingError:
|
||||||
|
raise Exception(
|
||||||
|
"Could not hash password of initial user with argon2id. If this is a system with less than 8 CPU cores, "
|
||||||
|
"you might need to disable argon2id by setting `passwords_argon2=off` in the `[django]` section of the "
|
||||||
|
"pretix.cfg configuration file."
|
||||||
|
)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +56,7 @@ class Migration(migrations.Migration):
|
|||||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='E-mail')),
|
('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='Email')),
|
||||||
('givenname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Given name')),
|
('givenname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Given name')),
|
||||||
('familyname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Family name')),
|
('familyname', models.CharField(blank=True, max_length=255, null=True, verbose_name='Family name')),
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||||
@@ -232,7 +240,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
||||||
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
||||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
|
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
|
||||||
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
||||||
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
||||||
('datetime', models.DateTimeField(verbose_name='Date')),
|
('datetime', models.DateTimeField(verbose_name='Date')),
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
('code', models.CharField(max_length=16, verbose_name='Order code')),
|
||||||
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
('status', models.CharField(choices=[('n', 'pending'), ('p', 'paid'), ('e', 'expired'), ('c', 'cancelled'), ('r', 'refunded')], max_length=3, verbose_name='Status')),
|
||||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
|
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
|
||||||
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
('locale', models.CharField(blank=True, max_length=32, null=True, verbose_name='Locale')),
|
||||||
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
('secret', models.CharField(default=pretix.base.models.orders.generate_secret, max_length=32)),
|
||||||
('datetime', models.DateTimeField(verbose_name='Date')),
|
('datetime', models.DateTimeField(verbose_name='Date')),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
|
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
|
||||||
('email', models.EmailField(max_length=254, verbose_name='E-mail address')),
|
('email', models.EmailField(max_length=254, verbose_name='Email address')),
|
||||||
('locale', models.CharField(default='en', max_length=190)),
|
('locale', models.CharField(default='en', max_length=190)),
|
||||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
|
||||||
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),
|
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
|
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
|
||||||
('email', models.EmailField(max_length=254, verbose_name='E-mail address')),
|
('email', models.EmailField(max_length=254, verbose_name='Email address')),
|
||||||
('locale', models.CharField(default='en', max_length=190)),
|
('locale', models.CharField(default='en', max_length=190)),
|
||||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
|
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
|
||||||
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),
|
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),
|
||||||
|
|||||||
+1
-1
@@ -163,7 +163,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('action_type', models.CharField(max_length=255)),
|
('action_type', models.CharField(max_length=255)),
|
||||||
('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
|
('method', models.CharField(choices=[('mail', 'Email')], max_length=255)),
|
||||||
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||||
to='pretixbase.Event')),
|
to='pretixbase.Event')),
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('action_type', models.CharField(max_length=255)),
|
('action_type', models.CharField(max_length=255)),
|
||||||
('method', models.CharField(choices=[('mail', 'E-mail')], max_length=255)),
|
('method', models.CharField(choices=[('mail', 'Email')], max_length=255)),
|
||||||
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')),
|
('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event')),
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
('enabled', models.BooleanField(default=True)),
|
('enabled', models.BooleanField(default=True)),
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2024-09-19 10:41
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||||
|
("pretixbase", "0271_itemcategory_cross_selling"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PrintLog",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("datetime", models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
("successful", models.BooleanField(default=True)),
|
||||||
|
("source", models.CharField(max_length=255)),
|
||||||
|
("type", models.CharField(max_length=255)),
|
||||||
|
("info", models.JSONField(default=dict)),
|
||||||
|
(
|
||||||
|
"api_token",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="pretixbase.teamapitoken",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"device",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="print_logs",
|
||||||
|
to="pretixbase.device",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"oauth_application",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"position",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="print_logs",
|
||||||
|
to="pretixbase.orderposition",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="print_logs",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ("-datetime",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2024-10-29 15:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_autocheckin(apps, schema_editor):
|
||||||
|
CheckinList = apps.get_model("pretixbase", "CheckinList")
|
||||||
|
AutoCheckinRule = apps.get_model("autocheckin", "AutoCheckinRule")
|
||||||
|
|
||||||
|
for cl in CheckinList.objects.filter(auto_checkin_sales_channels__isnull=False).select_related("event", "event__organizer"):
|
||||||
|
sales_channels = cl.auto_checkin_sales_channels.all()
|
||||||
|
all_sales_channels = cl.event.organizer.sales_channels.all()
|
||||||
|
|
||||||
|
if "pretix.plugins.autocheckin" not in cl.event.plugins:
|
||||||
|
cl.event.plugins = cl.event.plugins + ",pretix.plugins.autocheckin"
|
||||||
|
cl.event.save()
|
||||||
|
|
||||||
|
r = AutoCheckinRule.objects.get_or_create(
|
||||||
|
list=cl,
|
||||||
|
event=cl.event,
|
||||||
|
all_products=True,
|
||||||
|
all_payment_methods=True,
|
||||||
|
defaults=dict(
|
||||||
|
mode="placed",
|
||||||
|
all_sales_channels=len(sales_channels) == len(all_sales_channels),
|
||||||
|
)
|
||||||
|
)[0]
|
||||||
|
if len(sales_channels) != len(all_sales_channels):
|
||||||
|
r.limit_sales_channels.set(sales_channels)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0272_printlog"),
|
||||||
|
("autocheckin", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_autocheckin,
|
||||||
|
migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="checkinlist",
|
||||||
|
name="auto_checkin_sales_channels",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 4.2.8 on 2024-07-02 10:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"pretixbase",
|
||||||
|
"0273_remove_checkinlist_auto_checkin_sales_channels",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="invoiceline",
|
||||||
|
name="tax_code",
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="orderfee",
|
||||||
|
name="tax_code",
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="orderposition",
|
||||||
|
name="tax_code",
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="taxrule",
|
||||||
|
name="code",
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="transaction",
|
||||||
|
name="tax_code",
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -256,6 +256,9 @@ class SubeventColumnMixin:
|
|||||||
]
|
]
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
|
||||||
if value in self._subevent_cache:
|
if value in self._subevent_cache:
|
||||||
return self._subevent_cache[value]
|
return self._subevent_cache[value]
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ from pretix.base.signals import order_import_columns
|
|||||||
|
|
||||||
class EmailColumn(ImportColumn):
|
class EmailColumn(ImportColumn):
|
||||||
identifier = 'email'
|
identifier = 'email'
|
||||||
verbose_name = gettext_lazy('E-mail address')
|
verbose_name = gettext_lazy('Email address')
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
if value:
|
if value:
|
||||||
@@ -322,7 +322,7 @@ class AttendeeNamePart(ImportColumn):
|
|||||||
|
|
||||||
class AttendeeEmail(ImportColumn):
|
class AttendeeEmail(ImportColumn):
|
||||||
identifier = 'attendee_email'
|
identifier = 'attendee_email'
|
||||||
verbose_name = gettext_lazy('Attendee e-mail address')
|
verbose_name = gettext_lazy('Attendee email address')
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
if value:
|
if value:
|
||||||
@@ -441,6 +441,7 @@ class Price(DecimalColumnMixin, ImportColumn):
|
|||||||
position.price = p.gross
|
position.price = p.gross
|
||||||
position.tax_rule = position.item.tax_rule
|
position.tax_rule = position.item.tax_rule
|
||||||
position.tax_rate = p.rate
|
position.tax_rate = p.rate
|
||||||
|
position.tax_code = p.code
|
||||||
position.tax_value = p.tax
|
position.tax_value = p.tax
|
||||||
|
|
||||||
|
|
||||||
@@ -584,7 +585,7 @@ class SeatColumn(ImportColumn):
|
|||||||
raise ValidationError(_('Multiple matching seats were found.'))
|
raise ValidationError(_('Multiple matching seats were found.'))
|
||||||
except Seat.DoesNotExist:
|
except Seat.DoesNotExist:
|
||||||
raise ValidationError(_('No matching seat was found.'))
|
raise ValidationError(_('No matching seat was found.'))
|
||||||
if not value.is_available() or value in self._cached:
|
if not value.is_available(sales_channel=previous_values.get('sales_channel')) or value in self._cached:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('The seat you selected has already been taken. Please select a different seat.'))
|
_('The seat you selected has already been taken. Please select a different seat.'))
|
||||||
self._cached.add(value)
|
self._cached.add(value)
|
||||||
@@ -753,11 +754,11 @@ def get_order_import_columns(event):
|
|||||||
AttendeeState(event),
|
AttendeeState(event),
|
||||||
Price(event),
|
Price(event),
|
||||||
Secret(event),
|
Secret(event),
|
||||||
|
Saleschannel(event),
|
||||||
SeatColumn(event),
|
SeatColumn(event),
|
||||||
ValidFrom(event),
|
ValidFrom(event),
|
||||||
ValidUntil(event),
|
ValidUntil(event),
|
||||||
Locale(event),
|
Locale(event),
|
||||||
Saleschannel(event),
|
|
||||||
CheckinAttentionColumn(event),
|
CheckinAttentionColumn(event),
|
||||||
CheckinTextColumn(event),
|
CheckinTextColumn(event),
|
||||||
Expires(event),
|
Expires(event),
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
REQUIRED_FIELDS = []
|
REQUIRED_FIELDS = []
|
||||||
|
|
||||||
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
|
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
|
||||||
verbose_name=_('E-mail'), max_length=190)
|
verbose_name=_('Email'), max_length=190)
|
||||||
fullname = models.CharField(max_length=255, blank=True, null=True,
|
fullname = models.CharField(max_length=255, blank=True, null=True,
|
||||||
verbose_name=_('Full name'))
|
verbose_name=_('Full name'))
|
||||||
is_active = models.BooleanField(default=True,
|
is_active = models.BooleanField(default=True,
|
||||||
|
|||||||
@@ -99,14 +99,6 @@ class CheckinList(LoggedModel):
|
|||||||
verbose_name=_('Automatically check out everyone at'),
|
verbose_name=_('Automatically check out everyone at'),
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
auto_checkin_sales_channels = models.ManyToManyField(
|
|
||||||
"SalesChannel",
|
|
||||||
verbose_name=_('Sales channels to automatically check in'),
|
|
||||||
help_text=_('This option is deprecated and will be removed in the next months. As a replacement, our new plugin '
|
|
||||||
'"Auto check-in" can be used. When we remove this option, we will automatically migrate your event '
|
|
||||||
'to use the new plugin.'),
|
|
||||||
blank=True,
|
|
||||||
)
|
|
||||||
rules = models.JSONField(default=dict, blank=True)
|
rules = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='event__organizer')
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
@@ -141,7 +133,7 @@ class CheckinList(LoggedModel):
|
|||||||
return self.positions_query(ignore_status=False)
|
return self.positions_query(ignore_status=False)
|
||||||
|
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def positions_inside_query(self, ignore_status=False, at_time=None):
|
def _filter_positions_inside(self, qs, at_time=None):
|
||||||
if at_time is None:
|
if at_time is None:
|
||||||
c_q = []
|
c_q = []
|
||||||
else:
|
else:
|
||||||
@@ -149,7 +141,7 @@ class CheckinList(LoggedModel):
|
|||||||
|
|
||||||
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
|
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
|
||||||
# Use a simple approach that works on all databases
|
# Use a simple approach that works on all databases
|
||||||
qs = self.positions_query(ignore_status=ignore_status).annotate(
|
qs = qs.annotate(
|
||||||
last_entry=Subquery(
|
last_entry=Subquery(
|
||||||
Checkin.objects.filter(
|
Checkin.objects.filter(
|
||||||
*c_q,
|
*c_q,
|
||||||
@@ -202,7 +194,7 @@ class CheckinList(LoggedModel):
|
|||||||
.values("position_id", "type", "datetime", "cnt_exists_after")
|
.values("position_id", "type", "datetime", "cnt_exists_after")
|
||||||
.query.sql_with_params()
|
.query.sql_with_params()
|
||||||
)
|
)
|
||||||
return self.positions_query(ignore_status=ignore_status).filter(
|
return qs.filter(
|
||||||
pk__in=RawSQL(
|
pk__in=RawSQL(
|
||||||
f"""
|
f"""
|
||||||
SELECT "position_id"
|
SELECT "position_id"
|
||||||
@@ -214,6 +206,10 @@ class CheckinList(LoggedModel):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@scopes_disabled()
|
||||||
|
def positions_inside_query(self, ignore_status=False, at_time=None):
|
||||||
|
return self._filter_positions_inside(self.positions_query(ignore_status=ignore_status), at_time=at_time)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def positions_inside(self):
|
def positions_inside(self):
|
||||||
return self.positions_inside_query(None)
|
return self.positions_inside_query(None)
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class Customer(LoggedModel):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('E-mail'), max_length=190)
|
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('Email'), max_length=190)
|
||||||
phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number'))
|
phone = PhoneNumberField(null=True, blank=True, verbose_name=_('Phone number'))
|
||||||
password = models.CharField(verbose_name=_('Password'), max_length=128)
|
password = models.CharField(verbose_name=_('Password'), max_length=128)
|
||||||
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
||||||
@@ -392,7 +392,7 @@ class CustomerSSOClient(LoggedModel):
|
|||||||
SCOPE_CHOICES = (
|
SCOPE_CHOICES = (
|
||||||
('openid', _('OpenID Connect access (required)')),
|
('openid', _('OpenID Connect access (required)')),
|
||||||
('profile', _('Profile data (name, addresses)')),
|
('profile', _('Profile data (name, addresses)')),
|
||||||
('email', _('E-mail address')),
|
('email', _('Email address')),
|
||||||
('phone', _('Phone number')),
|
('phone', _('Phone number')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ from django.utils.crypto import get_random_string
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
|
|
||||||
from pretix.api.auth.devicesecurity import DEVICE_SECURITY_PROFILES
|
|
||||||
from pretix.base.models import LoggedModel
|
from pretix.base.models import LoggedModel
|
||||||
|
|
||||||
|
|
||||||
@@ -161,7 +160,6 @@ class Device(LoggedModel):
|
|||||||
)
|
)
|
||||||
security_profile = models.CharField(
|
security_profile = models.CharField(
|
||||||
max_length=190,
|
max_length=190,
|
||||||
choices=[(k, v.verbose_name) for k, v in DEVICE_SECURITY_PROFILES.items()],
|
|
||||||
default='full',
|
default='full',
|
||||||
null=True,
|
null=True,
|
||||||
blank=False
|
blank=False
|
||||||
|
|||||||
@@ -823,6 +823,9 @@ class Event(EventMixin, LoggedModel):
|
|||||||
self.save()
|
self.save()
|
||||||
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
||||||
|
|
||||||
|
if hasattr(other, 'alternative_domain_assignment'):
|
||||||
|
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
|
||||||
|
|
||||||
if not self.all_sales_channels:
|
if not self.all_sales_channels:
|
||||||
self.limit_sales_channels.set(
|
self.limit_sales_channels.set(
|
||||||
self.organizer.sales_channels.filter(
|
self.organizer.sales_channels.filter(
|
||||||
@@ -1024,10 +1027,9 @@ class Event(EventMixin, LoggedModel):
|
|||||||
|
|
||||||
checkin_list_map = {}
|
checkin_list_map = {}
|
||||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
|
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
|
||||||
'limit_products', 'auto_checkin_sales_channels'
|
'limit_products'
|
||||||
):
|
):
|
||||||
items = list(cl.limit_products.all())
|
items = list(cl.limit_products.all())
|
||||||
auto_checkin_sales_channels = list(cl.auto_checkin_sales_channels.all())
|
|
||||||
checkin_list_map[cl.pk] = cl
|
checkin_list_map[cl.pk] = cl
|
||||||
cl.pk = None
|
cl.pk = None
|
||||||
cl._prefetched_objects_cache = {}
|
cl._prefetched_objects_cache = {}
|
||||||
@@ -1039,8 +1041,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
cl.log_action('pretix.object.cloned')
|
cl.log_action('pretix.object.cloned')
|
||||||
for i in items:
|
for i in items:
|
||||||
cl.limit_products.add(item_map[i.pk])
|
cl.limit_products.add(item_map[i.pk])
|
||||||
if auto_checkin_sales_channels:
|
|
||||||
cl.auto_checkin_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in auto_checkin_sales_channels]))
|
|
||||||
|
|
||||||
if other.seating_plan:
|
if other.seating_plan:
|
||||||
if other.seating_plan.organizer_id == self.organizer_id:
|
if other.seating_plan.organizer_id == self.organizer_id:
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ class InvoiceLine(models.Model):
|
|||||||
tax_value = models.DecimalField(max_digits=13, decimal_places=2, default=Decimal('0.00'))
|
tax_value = models.DecimalField(max_digits=13, decimal_places=2, default=Decimal('0.00'))
|
||||||
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
|
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
|
||||||
tax_name = models.CharField(max_length=190)
|
tax_name = models.CharField(max_length=190)
|
||||||
|
tax_code = models.CharField(max_length=190, null=True, blank=True)
|
||||||
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
event_date_from = models.DateTimeField(null=True)
|
event_date_from = models.DateTimeField(null=True)
|
||||||
event_date_to = models.DateTimeField(null=True)
|
event_date_to = models.DateTimeField(null=True)
|
||||||
|
|||||||
@@ -837,7 +837,7 @@ class Item(LoggedModel):
|
|||||||
|
|
||||||
if not self.tax_rule:
|
if not self.tax_rule:
|
||||||
t = TaxedPrice(gross=price - bundled_sum, net=price - bundled_sum, tax=Decimal('0.00'),
|
t = TaxedPrice(gross=price - bundled_sum, net=price - bundled_sum, tax=Decimal('0.00'),
|
||||||
rate=Decimal('0.00'), name='')
|
rate=Decimal('0.00'), name='', code=None)
|
||||||
else:
|
else:
|
||||||
t = self.tax_rule.tax(price, base_price_is=base_price_is, invoice_address=invoice_address,
|
t = self.tax_rule.tax(price, base_price_is=base_price_is, invoice_address=invoice_address,
|
||||||
override_tax_rate=override_tax_rate, currency=currency or self.event.currency,
|
override_tax_rate=override_tax_rate, currency=currency or self.event.currency,
|
||||||
@@ -845,6 +845,7 @@ class Item(LoggedModel):
|
|||||||
|
|
||||||
if bundled_sum:
|
if bundled_sum:
|
||||||
t.name = "MIXED!"
|
t.name = "MIXED!"
|
||||||
|
t.code = None
|
||||||
t.gross += bundled_sum
|
t.gross += bundled_sum
|
||||||
t.net += bundled_sum_net
|
t.net += bundled_sum_net
|
||||||
t.tax += bundled_sum_tax
|
t.tax += bundled_sum_tax
|
||||||
@@ -1118,13 +1119,12 @@ class ItemVariation(models.Model):
|
|||||||
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
|
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
|
||||||
:type original_price: decimal.Decimal
|
:type original_price: decimal.Decimal
|
||||||
:param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after
|
:param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after
|
||||||
approval by an administrator
|
approval by an administrator
|
||||||
:type require_approval: bool
|
:type require_approval: bool
|
||||||
:param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored.
|
:param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored.
|
||||||
:type all_sales_channels: bool
|
:type all_sales_channels: bool
|
||||||
:param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on.
|
:param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on.
|
||||||
:type limit_sales_channels: list
|
:type limit_sales_channels: list
|
||||||
|
|
||||||
"""
|
"""
|
||||||
item = models.ForeignKey(
|
item = models.ForeignKey(
|
||||||
Item,
|
Item,
|
||||||
@@ -1259,7 +1259,7 @@ class ItemVariation(models.Model):
|
|||||||
|
|
||||||
if not self.item.tax_rule:
|
if not self.item.tax_rule:
|
||||||
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
||||||
rate=Decimal('0.00'), name='')
|
rate=Decimal('0.00'), name='', code=None)
|
||||||
else:
|
else:
|
||||||
t = self.item.tax_rule.tax(price, base_price_is=base_price_is, currency=currency,
|
t = self.item.tax_rule.tax(price, base_price_is=base_price_is, currency=currency,
|
||||||
override_tax_rate=override_tax_rate,
|
override_tax_rate=override_tax_rate,
|
||||||
@@ -1281,6 +1281,7 @@ class ItemVariation(models.Model):
|
|||||||
t.net += bprice.net - compare_price.net
|
t.net += bprice.net - compare_price.net
|
||||||
t.tax += bprice.tax - compare_price.tax
|
t.tax += bprice.tax - compare_price.tax
|
||||||
t.name = "MIXED!"
|
t.name = "MIXED!"
|
||||||
|
t.code = None
|
||||||
|
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|||||||
@@ -159,10 +159,24 @@ class Membership(models.Model):
|
|||||||
de = date_format(self.date_end, 'SHORT_DATE_FORMAT')
|
de = date_format(self.date_end, 'SHORT_DATE_FORMAT')
|
||||||
return f'{self.membership_type.name}: {self.attendee_name} ({ds} – {de})'
|
return f'{self.membership_type.name}: {self.attendee_name} ({ds} – {de})'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def percentage_used(self):
|
||||||
|
if self.membership_type.max_usages and self.usages:
|
||||||
|
return int(self.usages / self.membership_type.max_usages * 100)
|
||||||
|
return 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attendee_name(self):
|
def attendee_name(self):
|
||||||
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
|
return build_name(self.attendee_name_parts, fallback_scheme=lambda: self.customer.organizer.settings.name_scheme)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expired(self):
|
||||||
|
return time_machine_now() > self.date_end
|
||||||
|
|
||||||
|
@property
|
||||||
|
def not_yet_valid(self):
|
||||||
|
return time_machine_now() < self.date_start
|
||||||
|
|
||||||
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
|
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
|
||||||
if valid_from_not_chosen:
|
if valid_from_not_chosen:
|
||||||
return not self.canceled and self.date_end >= time_machine_now()
|
return not self.canceled and self.date_end >= time_machine_now()
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class NotificationSetting(models.Model):
|
|||||||
:type enabled: bool
|
:type enabled: bool
|
||||||
"""
|
"""
|
||||||
CHANNELS = (
|
CHANNELS = (
|
||||||
('mail', _('E-mail')),
|
('mail', _('Email')),
|
||||||
)
|
)
|
||||||
user = models.ForeignKey('User', on_delete=models.CASCADE,
|
user = models.ForeignKey('User', on_delete=models.CASCADE,
|
||||||
related_name='notification_settings')
|
related_name='notification_settings')
|
||||||
|
|||||||
@@ -62,9 +62,10 @@ from django.db.models.signals import post_delete
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.crypto import get_random_string, salted_hmac
|
from django.utils.crypto import get_random_string, salted_hmac
|
||||||
from django.utils.encoding import escape_uri_path
|
from django.utils.encoding import escape_uri_path, force_str
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.hashable import make_hashable
|
||||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
@@ -242,7 +243,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
)
|
)
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
verbose_name=_('E-mail')
|
verbose_name=_('Email')
|
||||||
)
|
)
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
@@ -317,7 +318,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
)
|
)
|
||||||
email_known_to_work = models.BooleanField(
|
email_known_to_work = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_('E-mail address verified')
|
verbose_name=_('Email address verified')
|
||||||
)
|
)
|
||||||
invoice_dirty = models.BooleanField(
|
invoice_dirty = models.BooleanField(
|
||||||
# Invoice needs to be re-issued when the order is paid again
|
# Invoice needs to be re-issued when the order is paid again
|
||||||
@@ -1256,7 +1257,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
keys = set(target_transaction_count.keys()) | set(current_transaction_count.keys())
|
keys = set(target_transaction_count.keys()) | set(current_transaction_count.keys())
|
||||||
create = []
|
create = []
|
||||||
for k in keys:
|
for k in keys:
|
||||||
positionid, itemid, variationid, subeventid, price, taxrate, taxruleid, taxvalue, feetype, internaltype = k
|
positionid, itemid, variationid, subeventid, price, taxrate, taxruleid, taxvalue, feetype, internaltype, taxcode = k
|
||||||
d = target_transaction_count[k] - current_transaction_count[k]
|
d = target_transaction_count[k] - current_transaction_count[k]
|
||||||
if d:
|
if d:
|
||||||
create.append(Transaction(
|
create.append(Transaction(
|
||||||
@@ -1272,6 +1273,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
tax_rate=taxrate,
|
tax_rate=taxrate,
|
||||||
tax_rule_id=taxruleid,
|
tax_rule_id=taxruleid,
|
||||||
tax_value=taxvalue,
|
tax_value=taxvalue,
|
||||||
|
tax_code=taxcode,
|
||||||
fee_type=feetype,
|
fee_type=feetype,
|
||||||
internal_type=internaltype,
|
internal_type=internaltype,
|
||||||
))
|
))
|
||||||
@@ -2275,6 +2277,7 @@ class OrderFee(models.Model):
|
|||||||
FEE_TYPE_SERVICE = "service"
|
FEE_TYPE_SERVICE = "service"
|
||||||
FEE_TYPE_CANCELLATION = "cancellation"
|
FEE_TYPE_CANCELLATION = "cancellation"
|
||||||
FEE_TYPE_INSURANCE = "insurance"
|
FEE_TYPE_INSURANCE = "insurance"
|
||||||
|
FEE_TYPE_LATE = "late"
|
||||||
FEE_TYPE_OTHER = "other"
|
FEE_TYPE_OTHER = "other"
|
||||||
FEE_TYPE_GIFTCARD = "giftcard"
|
FEE_TYPE_GIFTCARD = "giftcard"
|
||||||
FEE_TYPES = (
|
FEE_TYPES = (
|
||||||
@@ -2283,6 +2286,7 @@ class OrderFee(models.Model):
|
|||||||
(FEE_TYPE_SERVICE, _("Service fee")),
|
(FEE_TYPE_SERVICE, _("Service fee")),
|
||||||
(FEE_TYPE_CANCELLATION, _("Cancellation fee")),
|
(FEE_TYPE_CANCELLATION, _("Cancellation fee")),
|
||||||
(FEE_TYPE_INSURANCE, _("Insurance fee")),
|
(FEE_TYPE_INSURANCE, _("Insurance fee")),
|
||||||
|
(FEE_TYPE_LATE, _("Late fee")),
|
||||||
(FEE_TYPE_OTHER, _("Other fees")),
|
(FEE_TYPE_OTHER, _("Other fees")),
|
||||||
(FEE_TYPE_GIFTCARD, _("Gift card")),
|
(FEE_TYPE_GIFTCARD, _("Gift card")),
|
||||||
)
|
)
|
||||||
@@ -2311,6 +2315,10 @@ class OrderFee(models.Model):
|
|||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
|
tax_code = models.CharField(
|
||||||
|
max_length=190,
|
||||||
|
null=True, blank=True,
|
||||||
|
)
|
||||||
tax_value = models.DecimalField(
|
tax_value = models.DecimalField(
|
||||||
max_digits=13, decimal_places=2,
|
max_digits=13, decimal_places=2,
|
||||||
verbose_name=_('Tax value')
|
verbose_name=_('Tax value')
|
||||||
@@ -2338,6 +2346,16 @@ class OrderFee(models.Model):
|
|||||||
self._transaction_key_reset()
|
self._transaction_key_reset()
|
||||||
return super().refresh_from_db(using, fields)
|
return super().refresh_from_db(using, fields)
|
||||||
|
|
||||||
|
def get_tax_code_display(self):
|
||||||
|
from pretix.base.models.tax import get_tax_code_labels
|
||||||
|
|
||||||
|
if self.tax_code:
|
||||||
|
choices_dict = get_tax_code_labels()
|
||||||
|
return force_str(
|
||||||
|
choices_dict.get(make_hashable(self.tax_code), self.tax_code), strings_only=True
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|
||||||
def _transaction_key_reset(self):
|
def _transaction_key_reset(self):
|
||||||
self.__initial_transaction_key = Transaction.key(self)
|
self.__initial_transaction_key = Transaction.key(self)
|
||||||
self.__initial_canceled = self.canceled
|
self.__initial_canceled = self.canceled
|
||||||
@@ -2368,9 +2386,11 @@ class OrderFee(models.Model):
|
|||||||
if self.tax_rule:
|
if self.tax_rule:
|
||||||
tax = self.tax_rule.tax(self.value, base_price_is='gross', invoice_address=ia, force_fixed_gross_price=True)
|
tax = self.tax_rule.tax(self.value, base_price_is='gross', invoice_address=ia, force_fixed_gross_price=True)
|
||||||
self.tax_rate = tax.rate
|
self.tax_rate = tax.rate
|
||||||
|
self.tax_code = tax.code
|
||||||
self.tax_value = tax.tax
|
self.tax_value = tax.tax
|
||||||
else:
|
else:
|
||||||
self.tax_value = Decimal('0.00')
|
self.tax_value = Decimal('0.00')
|
||||||
|
self.tax_code = None
|
||||||
self.tax_rate = Decimal('0.00')
|
self.tax_rate = Decimal('0.00')
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@@ -2379,6 +2399,7 @@ class OrderFee(models.Model):
|
|||||||
|
|
||||||
if self.tax_rate is None:
|
if self.tax_rate is None:
|
||||||
self._calculate_tax()
|
self._calculate_tax()
|
||||||
|
|
||||||
self.order.touch()
|
self.order.touch()
|
||||||
|
|
||||||
if not self.get_deferred_fields():
|
if not self.get_deferred_fields():
|
||||||
@@ -2466,6 +2487,10 @@ class OrderPosition(AbstractPosition):
|
|||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
|
tax_code = models.CharField(
|
||||||
|
max_length=190,
|
||||||
|
null=True, blank=True,
|
||||||
|
)
|
||||||
tax_value = models.DecimalField(
|
tax_value = models.DecimalField(
|
||||||
max_digits=13, decimal_places=2,
|
max_digits=13, decimal_places=2,
|
||||||
verbose_name=_('Tax value')
|
verbose_name=_('Tax value')
|
||||||
@@ -2523,6 +2548,16 @@ class OrderPosition(AbstractPosition):
|
|||||||
models.UniqueConstraint("organizer", "secret", name="orderposition_organizer_secret_uniq")
|
models.UniqueConstraint("organizer", "secret", name="orderposition_organizer_secret_uniq")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_tax_code_display(self):
|
||||||
|
from pretix.base.models.tax import get_tax_code_labels
|
||||||
|
|
||||||
|
if self.tax_code:
|
||||||
|
choices_dict = get_tax_code_labels()
|
||||||
|
return force_str(
|
||||||
|
choices_dict.get(make_hashable(self.tax_code), self.tax_code), strings_only=True
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def sort_key(self):
|
def sort_key(self):
|
||||||
return self.addon_to.positionid if self.addon_to else self.positionid, self.addon_to_id or 0, self.positionid
|
return self.addon_to.positionid if self.addon_to else self.positionid, self.addon_to_id or 0, self.positionid
|
||||||
@@ -2695,11 +2730,13 @@ class OrderPosition(AbstractPosition):
|
|||||||
if self.tax_rule:
|
if self.tax_rule:
|
||||||
tax = self.tax_rule.tax(self.price, invoice_address=ia, base_price_is='gross', force_fixed_gross_price=True)
|
tax = self.tax_rule.tax(self.price, invoice_address=ia, base_price_is='gross', force_fixed_gross_price=True)
|
||||||
self.tax_rate = tax.rate
|
self.tax_rate = tax.rate
|
||||||
|
self.tax_code = tax.code
|
||||||
self.tax_value = tax.tax
|
self.tax_value = tax.tax
|
||||||
if tax.gross != self.price:
|
if tax.gross != self.price:
|
||||||
raise ValueError('Invalid tax calculation')
|
raise ValueError('Invalid tax calculation')
|
||||||
else:
|
else:
|
||||||
self.tax_value = Decimal('0.00')
|
self.tax_value = Decimal('0.00')
|
||||||
|
self.tax_code = None
|
||||||
self.tax_rate = Decimal('0.00')
|
self.tax_rate = Decimal('0.00')
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@@ -2970,6 +3007,10 @@ class Transaction(models.Model):
|
|||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
|
tax_code = models.CharField(
|
||||||
|
max_length=190,
|
||||||
|
null=True, blank=True,
|
||||||
|
)
|
||||||
tax_value = models.DecimalField(
|
tax_value = models.DecimalField(
|
||||||
max_digits=13, decimal_places=2,
|
max_digits=13, decimal_places=2,
|
||||||
verbose_name=_('Tax value')
|
verbose_name=_('Tax value')
|
||||||
@@ -2990,17 +3031,27 @@ class Transaction(models.Model):
|
|||||||
raise ValidationError('Should set either item or fee type')
|
raise ValidationError('Should set either item or fee type')
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_tax_code_display(self):
|
||||||
|
from pretix.base.models.tax import get_tax_code_labels
|
||||||
|
|
||||||
|
if self.tax_code:
|
||||||
|
choices_dict = get_tax_code_labels()
|
||||||
|
return force_str(
|
||||||
|
choices_dict.get(make_hashable(self.tax_code), self.tax_code), strings_only=True
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def key(obj):
|
def key(obj):
|
||||||
if isinstance(obj, Transaction):
|
if isinstance(obj, Transaction):
|
||||||
return (obj.positionid, obj.item_id, obj.variation_id, obj.subevent_id, obj.price, obj.tax_rate,
|
return (obj.positionid, obj.item_id, obj.variation_id, obj.subevent_id, obj.price, obj.tax_rate,
|
||||||
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type)
|
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type, obj.tax_code)
|
||||||
elif isinstance(obj, OrderPosition):
|
elif isinstance(obj, OrderPosition):
|
||||||
return (obj.positionid, obj.item_id, obj.variation_id, obj.subevent_id, obj.price, obj.tax_rate,
|
return (obj.positionid, obj.item_id, obj.variation_id, obj.subevent_id, obj.price, obj.tax_rate,
|
||||||
obj.tax_rule_id, obj.tax_value, None, None)
|
obj.tax_rule_id, obj.tax_value, None, None, obj.tax_code)
|
||||||
elif isinstance(obj, OrderFee):
|
elif isinstance(obj, OrderFee):
|
||||||
return (None, None, None, None, obj.value, obj.tax_rate,
|
return (None, None, None, None, obj.value, obj.tax_rate,
|
||||||
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type)
|
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type, obj.tax_code)
|
||||||
raise ValueError('invalid state') # noqa
|
raise ValueError('invalid state') # noqa
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -3164,6 +3215,7 @@ class CartPosition(AbstractPosition):
|
|||||||
if line_price.gross != self.line_price_gross or line_price.rate != self.tax_rate:
|
if line_price.gross != self.line_price_gross or line_price.rate != self.tax_rate:
|
||||||
self.line_price_gross = line_price.gross
|
self.line_price_gross = line_price.gross
|
||||||
self.tax_rate = line_price.rate
|
self.tax_rate = line_price.rate
|
||||||
|
self.tax_code = line_price.code
|
||||||
self.save(update_fields=['line_price_gross', 'tax_rate'])
|
self.save(update_fields=['line_price_gross', 'tax_rate'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -3204,9 +3256,9 @@ class InvoiceAddress(models.Model):
|
|||||||
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
|
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
|
||||||
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
||||||
name_parts = models.JSONField(default=dict)
|
name_parts = models.JSONField(default=dict)
|
||||||
street = models.TextField(verbose_name=_('Address'), blank=False)
|
street = models.TextField(verbose_name=_('Address'), blank=True)
|
||||||
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
|
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True)
|
||||||
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
|
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True)
|
||||||
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
|
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
|
||||||
country = FastCountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
|
country = FastCountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
|
||||||
countries=CachedCountries)
|
countries=CachedCountries)
|
||||||
@@ -3391,6 +3443,74 @@ class BlockedTicketSecret(models.Model):
|
|||||||
unique_together = (('event', 'secret'),)
|
unique_together = (('event', 'secret'),)
|
||||||
|
|
||||||
|
|
||||||
|
class PrintLog(models.Model):
|
||||||
|
"""
|
||||||
|
A print log object is created when a ticket or badge is printed with our apps.
|
||||||
|
"""
|
||||||
|
TYPE_BADGE = 'badge'
|
||||||
|
TYPE_TICKET = 'ticket'
|
||||||
|
TYPE_CERTIFICATE = 'certificate'
|
||||||
|
TYPE_OTHER = 'other'
|
||||||
|
PRINT_TYPES = (
|
||||||
|
(TYPE_BADGE, _('Badge')),
|
||||||
|
(TYPE_TICKET, _('Ticket')),
|
||||||
|
(TYPE_CERTIFICATE, _('Certificate')),
|
||||||
|
(TYPE_OTHER, _('Other')),
|
||||||
|
)
|
||||||
|
|
||||||
|
position = models.ForeignKey(
|
||||||
|
'pretixbase.OrderPosition',
|
||||||
|
related_name='print_logs',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
successful = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Datetime of checkin, might be different from created if past scans are uploaded
|
||||||
|
datetime = models.DateTimeField(default=now)
|
||||||
|
|
||||||
|
# Datetime of creation on server
|
||||||
|
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
||||||
|
|
||||||
|
# Who printed?
|
||||||
|
device = models.ForeignKey('Device', related_name='print_logs', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
|
user = models.ForeignKey('User', related_name='print_logs', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
|
api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
|
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
# Source = Tag field with undefined values, e.g. name of app ("pretixscan")
|
||||||
|
source = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
# Type = Type of object printed ("badge", "ticket")
|
||||||
|
type = models.CharField(max_length=255, choices=PRINT_TYPES)
|
||||||
|
|
||||||
|
info = models.JSONField(default=dict)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='position__order__event__organizer')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = (('-datetime'),)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<PrintLog: pos {} at {} from {}>".format(
|
||||||
|
self.position, self.datetime, self.source
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
super().save(**kwargs)
|
||||||
|
if self.position:
|
||||||
|
self.position.order.touch()
|
||||||
|
|
||||||
|
def delete(self, **kwargs):
|
||||||
|
super().delete(**kwargs)
|
||||||
|
self.position.order.touch()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_late_upload(self):
|
||||||
|
return self.created and abs(self.created - self.datetime) > timedelta(minutes=2)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=CachedTicket)
|
@receiver(post_delete, sender=CachedTicket)
|
||||||
def cachedticket_delete(sender, instance, **kwargs):
|
def cachedticket_delete(sender, instance, **kwargs):
|
||||||
if instance.file:
|
if instance.file:
|
||||||
|
|||||||
@@ -53,6 +53,30 @@ class SeatingPlanLayoutValidator:
|
|||||||
e = str(e).replace('%', '%%')
|
e = str(e).replace('%', '%%')
|
||||||
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(e))
|
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
seat_guids = set()
|
||||||
|
for z in val["zones"]:
|
||||||
|
for r in z["rows"]:
|
||||||
|
for s in r["seats"]:
|
||||||
|
if not s.get("seat_guid"):
|
||||||
|
raise ValidationError(
|
||||||
|
_("Seat with zone {zone}, row {row}, and number {number} has no seat ID.").format(
|
||||||
|
zone=z["name"],
|
||||||
|
row=r["row_number"],
|
||||||
|
number=s["seat_number"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif s["seat_guid"] in seat_guids:
|
||||||
|
raise ValidationError(
|
||||||
|
_("Multiple seats have the same ID: {id}").format(
|
||||||
|
id=s["seat_guid"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
seat_guids.add(s["seat_guid"])
|
||||||
|
except ValidationError as e:
|
||||||
|
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(", ".join(e.message for e in e.error_list)))
|
||||||
|
|
||||||
|
|
||||||
class SeatingPlan(LoggedModel):
|
class SeatingPlan(LoggedModel):
|
||||||
"""
|
"""
|
||||||
|
|||||||
+220
-12
@@ -21,6 +21,7 @@
|
|||||||
#
|
#
|
||||||
import json
|
import json
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import jsonschema
|
import jsonschema
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
@@ -30,8 +31,9 @@ from django.db import models
|
|||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
from django.utils.formats import localize
|
from django.utils.formats import localize
|
||||||
from django.utils.functional import lazy
|
from django.utils.functional import lazy
|
||||||
|
from django.utils.hashable import make_hashable
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext
|
from django.utils.translation import gettext_lazy as _, pgettext, pgettext_lazy
|
||||||
from i18nfield.fields import I18nCharField
|
from i18nfield.fields import I18nCharField
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ from pretix.helpers.countries import FastCountryField
|
|||||||
|
|
||||||
|
|
||||||
class TaxedPrice:
|
class TaxedPrice:
|
||||||
def __init__(self, *, gross: Decimal, net: Decimal, tax: Decimal, rate: Decimal, name: str):
|
def __init__(self, *, gross: Decimal, net: Decimal, tax: Decimal, rate: Decimal, name: str, code: Optional[str]):
|
||||||
if net + tax != gross:
|
if net + tax != gross:
|
||||||
raise ValueError('Net value and tax value need to add to the gross value')
|
raise ValueError('Net value and tax value need to add to the gross value')
|
||||||
self.gross = gross
|
self.gross = gross
|
||||||
@@ -50,6 +52,7 @@ class TaxedPrice:
|
|||||||
self.tax = tax
|
self.tax = tax
|
||||||
self.rate = rate
|
self.rate = rate
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.code = code
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '{} + {}% = {}'.format(localize(self.net), localize(self.rate), localize(self.gross))
|
return '{} + {}% = {}'.format(localize(self.net), localize(self.rate), localize(self.gross))
|
||||||
@@ -72,6 +75,7 @@ class TaxedPrice:
|
|||||||
tax=newgross - newnet,
|
tax=newgross - newnet,
|
||||||
rate=self.rate,
|
rate=self.rate,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
|
code=self.code,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __mul__(self, other):
|
def __mul__(self, other):
|
||||||
@@ -85,6 +89,7 @@ class TaxedPrice:
|
|||||||
tax=newgross - newnet,
|
tax=newgross - newnet,
|
||||||
rate=self.rate,
|
rate=self.rate,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
|
code=self.code,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
@@ -93,7 +98,8 @@ class TaxedPrice:
|
|||||||
self.net == other.net and
|
self.net == other.net and
|
||||||
self.tax == other.tax and
|
self.tax == other.tax and
|
||||||
self.rate == other.rate and
|
self.rate == other.rate and
|
||||||
self.name == other.name
|
self.name == other.name and
|
||||||
|
self.code == other.code
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -102,7 +108,8 @@ TAXED_ZERO = TaxedPrice(
|
|||||||
net=Decimal('0.00'),
|
net=Decimal('0.00'),
|
||||||
tax=Decimal('0.00'),
|
tax=Decimal('0.00'),
|
||||||
rate=Decimal('0.00'),
|
rate=Decimal('0.00'),
|
||||||
name=''
|
name='',
|
||||||
|
code=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
EU_COUNTRIES = {
|
EU_COUNTRIES = {
|
||||||
@@ -125,6 +132,152 @@ VAT_ID_COUNTRIES = EU_COUNTRIES | {'CH', 'NO'}
|
|||||||
format_html_lazy = lazy(format_html, str)
|
format_html_lazy = lazy(format_html, str)
|
||||||
|
|
||||||
|
|
||||||
|
TAX_CODE_LISTS = (
|
||||||
|
# Sources:
|
||||||
|
# https://ec.europa.eu/digital-building-blocks/sites/display/DIGITAL/Registry+of+supporting+artefacts+to+implement+EN16931#RegistryofsupportingartefactstoimplementEN16931-Codelists#RegistryofsupportingartefactstoimplementEN16931-Codelists
|
||||||
|
# https://docs.peppol.eu/poacc/billing/3.0/codelist/vatex/
|
||||||
|
# https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL5305/
|
||||||
|
# https://www.bzst.de/DE/Unternehmen/Aussenpruefungen/DigitaleSchnittstelleFinV/digitaleschnittstellefinv_node.html#js-toc-entry2
|
||||||
|
#
|
||||||
|
# !! When changed, also update tax-rules-custom.schema.json and doc/api/resources/taxrules.rst !!
|
||||||
|
(
|
||||||
|
_("Standard rates"),
|
||||||
|
(
|
||||||
|
# Standard rate in any country, such as 19% in Germany or 20% in Austria
|
||||||
|
# DSFinV-K mapping: 1
|
||||||
|
("S/standard", pgettext_lazy("tax_code", "Standard rate")),
|
||||||
|
|
||||||
|
# Reduced rate in any country, such as 7% in Germany or both 10% and 13% in Austria
|
||||||
|
# DSFinV-K mapping: 2
|
||||||
|
("S/reduced", pgettext_lazy("tax_code", "Reduced rate")),
|
||||||
|
|
||||||
|
# Averaged rate, for example Germany § 24 (1) Nr. 3 UStG "für die übrigen Umsätze" in agricultural and silvicultural businesses
|
||||||
|
# DSFinV-K mapping: 3
|
||||||
|
("S/averaged", pgettext_lazy("tax_code", "Averaged rate (other revenue in a agricultural and silvicultural business)")),
|
||||||
|
|
||||||
|
# We ignore the German special case of the actual silvicultural products as they won't be sold through pretix (DSFinV-K mapping: 4)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Reverse charge"),
|
||||||
|
(
|
||||||
|
("AE", pgettext_lazy("tax_code", "Reverse charge")),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Tax free"),
|
||||||
|
(
|
||||||
|
# DSFinV-K mapping: 5
|
||||||
|
("O", pgettext_lazy("tax_code", "Services outside of scope of tax")),
|
||||||
|
|
||||||
|
# DSFinV-K mapping: 6
|
||||||
|
("E", pgettext_lazy("tax_code", "Exempt from tax (no reason given)")),
|
||||||
|
|
||||||
|
# DSFinV-K mapping: 6
|
||||||
|
("Z", pgettext_lazy("tax_code", "Zero-rated goods")),
|
||||||
|
|
||||||
|
# DSFinV-K mapping: 5
|
||||||
|
("G", pgettext_lazy("tax_code", "Free export item, VAT not charged")),
|
||||||
|
|
||||||
|
# DSFinV-K mapping: 6?
|
||||||
|
("K", pgettext_lazy("tax_code", "VAT exempt for EEA intra-community supply of goods and services")),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Special cases"),
|
||||||
|
(
|
||||||
|
("L", pgettext_lazy("tax_code", "Canary Islands general indirect tax")),
|
||||||
|
("M", pgettext_lazy("tax_code", "Tax for production, services and importation in Ceuta and Melilla")),
|
||||||
|
("B", pgettext_lazy("tax_code", "Transferred (VAT), only in Italy")),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Exempt with specific reason"),
|
||||||
|
(
|
||||||
|
("E/VATEX-EU-79-C",
|
||||||
|
pgettext_lazy("tax_code", "Exempt based on article 79, point c of Council Directive 2006/112/EC")),
|
||||||
|
*[
|
||||||
|
(
|
||||||
|
f"E/VATEX-EU-132-1{letter.upper()}",
|
||||||
|
lazy(
|
||||||
|
lambda let: pgettext(
|
||||||
|
"tax_code",
|
||||||
|
"Exempt based on article {article}, section {section} ({letter}) of Council "
|
||||||
|
"Directive 2006/112/EC"
|
||||||
|
).format(article="132", section="1", letter=let),
|
||||||
|
str
|
||||||
|
)(letter)
|
||||||
|
) for letter in ("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q")
|
||||||
|
],
|
||||||
|
*[
|
||||||
|
(
|
||||||
|
f"E/VATEX-EU-143-1{letter.upper()}",
|
||||||
|
lazy(
|
||||||
|
lambda let: pgettext(
|
||||||
|
"tax_code",
|
||||||
|
"Exempt based on article {article}, section {section} ({letter}) of Council "
|
||||||
|
"Directive 2006/112/EC"
|
||||||
|
).format(article="143", section="1", letter=let),
|
||||||
|
str
|
||||||
|
)(letter)
|
||||||
|
) for letter in ("a", "b", "c", "d", "e", "f", "fa", "g", "h", "i", "j", "k", "l")
|
||||||
|
],
|
||||||
|
*[
|
||||||
|
(
|
||||||
|
f"E/VATEX-EU-148-{letter.upper()}",
|
||||||
|
lazy(
|
||||||
|
lambda let: pgettext(
|
||||||
|
"tax_code",
|
||||||
|
"Exempt based on article {article}, section ({letter}) of Council "
|
||||||
|
"Directive 2006/112/EC"
|
||||||
|
).format(article="148", letter=let),
|
||||||
|
str
|
||||||
|
)(letter)
|
||||||
|
) for letter in ("a", "b", "c", "d", "e", "f", "g")
|
||||||
|
],
|
||||||
|
*[
|
||||||
|
(
|
||||||
|
f"E/VATEX-EU-151-1{letter.upper()}",
|
||||||
|
lazy(
|
||||||
|
lambda let: pgettext(
|
||||||
|
"tax_code",
|
||||||
|
"Exempt based on article {article}, section {section} ({letter}) of Council "
|
||||||
|
"Directive 2006/112/EC"
|
||||||
|
).format(article="151", section="1", letter=let),
|
||||||
|
str
|
||||||
|
)(letter)
|
||||||
|
) for letter in ("a", "aa", "b", "c", "d", "e")
|
||||||
|
],
|
||||||
|
("E/VATEX-EU-309",
|
||||||
|
pgettext_lazy("tax_code", "Exempt based on article 309 of Council Directive 2006/112/EC")),
|
||||||
|
("E/VATEX-EU-D",
|
||||||
|
pgettext_lazy("tax_code", "Intra-Community acquisition from second hand means of transport")),
|
||||||
|
("E/VATEX-EU-F",
|
||||||
|
pgettext_lazy("tax_code", "Intra-Community acquisition of second hand goods")),
|
||||||
|
("E/VATEX-EU-I",
|
||||||
|
pgettext_lazy("tax_code", "Intra-Community acquisition of works of art")),
|
||||||
|
("E/VATEX-EU-J",
|
||||||
|
pgettext_lazy("tax_code", "Intra-Community acquisition of collectors items and antiques")),
|
||||||
|
("E/VATEX-FR-FRANCHISE",
|
||||||
|
pgettext_lazy("tax_code", "France domestic VAT franchise in base")),
|
||||||
|
("E/VATEX-FR-CNWVAT",
|
||||||
|
pgettext_lazy("tax_code", "France domestic Credit Notes without VAT, due to supplier forfeit of VAT for discount")),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_tax_code_labels():
|
||||||
|
flat = []
|
||||||
|
for choice, value in TAX_CODE_LISTS:
|
||||||
|
if isinstance(value, (list, tuple)):
|
||||||
|
flat.extend(value)
|
||||||
|
else:
|
||||||
|
flat.append((choice, value))
|
||||||
|
|
||||||
|
return dict(make_hashable(flat))
|
||||||
|
|
||||||
|
|
||||||
def is_eu_country(cc):
|
def is_eu_country(cc):
|
||||||
cc = str(cc)
|
cc = str(cc)
|
||||||
return cc in EU_COUNTRIES
|
return cc in EU_COUNTRIES
|
||||||
@@ -173,6 +326,14 @@ class TaxRule(LoggedModel):
|
|||||||
help_text=_('Should be short, e.g. "VAT"'),
|
help_text=_('Should be short, e.g. "VAT"'),
|
||||||
max_length=190,
|
max_length=190,
|
||||||
)
|
)
|
||||||
|
code = models.CharField(
|
||||||
|
verbose_name=_('Tax code'),
|
||||||
|
help_text=_('If you help us understand what this tax rules legally is, we can use this information for '
|
||||||
|
'eInvoices, exporting to accounting system, etc.'),
|
||||||
|
null=True, blank=True,
|
||||||
|
max_length=190,
|
||||||
|
choices=TAX_CODE_LISTS,
|
||||||
|
)
|
||||||
rate = models.DecimalField(
|
rate = models.DecimalField(
|
||||||
max_digits=10,
|
max_digits=10,
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
@@ -250,6 +411,16 @@ class TaxRule(LoggedModel):
|
|||||||
if self.eu_reverse_charge and not self.home_country:
|
if self.eu_reverse_charge and not self.home_country:
|
||||||
raise ValidationError(_('You need to set your home country to use the reverse charge feature.'))
|
raise ValidationError(_('You need to set your home country to use the reverse charge feature.'))
|
||||||
|
|
||||||
|
if self.rate != Decimal("0.00") and self.code and (self.code.split("/")[0] in ("O", "E", "Z", "G", "K", "AE")):
|
||||||
|
raise ValidationError({
|
||||||
|
"code": _("A combination of this tax code with a non-zero tax rate does not make sense.")
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.rate == Decimal("0.00") and self.code and (self.code.split("/")[0] in ("S", "L", "M", "B")):
|
||||||
|
raise ValidationError({
|
||||||
|
"code": _("A combination of this tax code with a zero tax rate does not make sense.")
|
||||||
|
})
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.price_includes_tax:
|
if self.price_includes_tax:
|
||||||
s = _('incl. {rate}% {name}').format(rate=self.rate, name=self.name)
|
s = _('incl. {rate}% {name}').format(rate=self.rate, name=self.name)
|
||||||
@@ -276,8 +447,9 @@ class TaxRule(LoggedModel):
|
|||||||
return Decimal(rule.get('rate'))
|
return Decimal(rule.get('rate'))
|
||||||
return Decimal(self.rate)
|
return Decimal(self.rate)
|
||||||
|
|
||||||
def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, invoice_address=None,
|
def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, override_tax_code=None,
|
||||||
subtract_from_gross=Decimal('0.00'), gross_price_is_tax_rate: Decimal = None, force_fixed_gross_price=False):
|
invoice_address=None, subtract_from_gross=Decimal('0.00'), gross_price_is_tax_rate: Decimal = None,
|
||||||
|
force_fixed_gross_price=False):
|
||||||
from .event import Event
|
from .event import Event
|
||||||
try:
|
try:
|
||||||
currency = currency or self.event.currency
|
currency = currency or self.event.currency
|
||||||
@@ -285,6 +457,13 @@ class TaxRule(LoggedModel):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
rate = Decimal(self.rate)
|
rate = Decimal(self.rate)
|
||||||
|
code = self.code
|
||||||
|
|
||||||
|
if override_tax_code is not None:
|
||||||
|
code = override_tax_code
|
||||||
|
elif invoice_address:
|
||||||
|
code = self.tax_code_for(invoice_address)
|
||||||
|
|
||||||
if override_tax_rate is not None:
|
if override_tax_rate is not None:
|
||||||
rate = override_tax_rate
|
rate = override_tax_rate
|
||||||
elif invoice_address:
|
elif invoice_address:
|
||||||
@@ -317,11 +496,8 @@ class TaxRule(LoggedModel):
|
|||||||
if rate == Decimal('0.00'):
|
if rate == Decimal('0.00'):
|
||||||
gross = _limit_subtract(base_price, subtract_from_gross)
|
gross = _limit_subtract(base_price, subtract_from_gross)
|
||||||
return TaxedPrice(
|
return TaxedPrice(
|
||||||
net=gross,
|
net=gross, gross=gross, tax=Decimal('0.00'),
|
||||||
gross=gross,
|
rate=rate, name=self.name, code=code,
|
||||||
tax=Decimal('0.00'),
|
|
||||||
rate=rate,
|
|
||||||
name=self.name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if base_price_is == 'auto':
|
if base_price_is == 'auto':
|
||||||
@@ -346,7 +522,7 @@ class TaxRule(LoggedModel):
|
|||||||
|
|
||||||
return TaxedPrice(
|
return TaxedPrice(
|
||||||
net=net, gross=gross, tax=gross - net,
|
net=net, gross=gross, tax=gross - net,
|
||||||
rate=rate, name=self.name
|
rate=rate, name=self.name, code=code,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -427,6 +603,38 @@ class TaxRule(LoggedModel):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def tax_code_for(self, invoice_address):
|
||||||
|
if self._custom_rules:
|
||||||
|
rule = self.get_matching_rule(invoice_address)
|
||||||
|
if rule.get("code"):
|
||||||
|
return rule["code"]
|
||||||
|
if rule.get("action", "vat") == "reverse":
|
||||||
|
return "AE"
|
||||||
|
return self.code
|
||||||
|
|
||||||
|
if not self.eu_reverse_charge:
|
||||||
|
# No reverse charge rules? Always apply VAT!
|
||||||
|
return self.code
|
||||||
|
|
||||||
|
if not invoice_address or not invoice_address.country:
|
||||||
|
# No country specified? Always apply VAT!
|
||||||
|
return self.code
|
||||||
|
|
||||||
|
if not is_eu_country(invoice_address.country):
|
||||||
|
# Non-EU country? "Non-taxable" since not in scope
|
||||||
|
return "O"
|
||||||
|
|
||||||
|
if invoice_address.country == self.home_country:
|
||||||
|
# Within same EU country? Always apply VAT!
|
||||||
|
return self.code
|
||||||
|
|
||||||
|
if invoice_address.is_business and invoice_address.vat_id and invoice_address.vat_id_validated:
|
||||||
|
# Reverse charge case
|
||||||
|
return "AE"
|
||||||
|
|
||||||
|
# Consumer in different EU country / invalid VAT
|
||||||
|
return self.code
|
||||||
|
|
||||||
def _tax_applicable(self, invoice_address):
|
def _tax_applicable(self, invoice_address):
|
||||||
if self._custom_rules:
|
if self._custom_rules:
|
||||||
rule = self.get_matching_rule(invoice_address)
|
rule = self.get_matching_rule(invoice_address)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class WaitingListEntry(LoggedModel):
|
|||||||
blank=True, default=dict
|
blank=True, default=dict
|
||||||
)
|
)
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
verbose_name=_("E-mail address")
|
verbose_name=_("Email address")
|
||||||
)
|
)
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
|
|||||||
+34
-33
@@ -1419,50 +1419,51 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
|
def _add_giftcard_to_cart(self, cs, gc):
|
||||||
from pretix.base.services.cart import add_payment_to_cart
|
from pretix.base.services.cart import add_payment_to_cart_session
|
||||||
|
|
||||||
|
if gc.currency != self.event.currency:
|
||||||
|
raise ValidationError(_("This gift card does not support this currency."))
|
||||||
|
if gc.testmode and not self.event.testmode:
|
||||||
|
raise ValidationError(_("This gift card can only be used in test mode."))
|
||||||
|
if not gc.testmode and self.event.testmode:
|
||||||
|
raise ValidationError(_("Only test gift cards can be used in test mode."))
|
||||||
|
if gc.expires and gc.expires < time_machine_now():
|
||||||
|
raise ValidationError(_("This gift card is no longer valid."))
|
||||||
|
if gc.value <= Decimal("0.00"):
|
||||||
|
raise ValidationError(_("All credit on this gift card has been used."))
|
||||||
|
|
||||||
|
for p in cs.get('payments', []):
|
||||||
|
if p['provider'] == self.identifier and p['info_data']['gift_card'] == gc.pk:
|
||||||
|
raise ValidationError(_("This gift card is already used for your payment."))
|
||||||
|
|
||||||
|
add_payment_to_cart_session(
|
||||||
|
cs,
|
||||||
|
self,
|
||||||
|
max_value=gc.value,
|
||||||
|
info_data={
|
||||||
|
'gift_card': gc.pk,
|
||||||
|
'gift_card_secret': gc.secret,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
|
||||||
for p in get_cart(request):
|
for p in get_cart(request):
|
||||||
if p.item.issue_giftcard:
|
if p.item.issue_giftcard:
|
||||||
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
|
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
|
||||||
return
|
return
|
||||||
|
|
||||||
cs = cart_session(request)
|
|
||||||
try:
|
try:
|
||||||
gc = self.event.organizer.accepted_gift_cards.get(
|
gc = self.event.organizer.accepted_gift_cards.get(
|
||||||
secret=request.POST.get("giftcard").strip()
|
secret=request.POST.get("giftcard").strip()
|
||||||
)
|
)
|
||||||
if gc.currency != self.event.currency:
|
cs = cart_session(request)
|
||||||
messages.error(request, _("This gift card does not support this currency."))
|
try:
|
||||||
|
self._add_giftcard_to_cart(cs, gc)
|
||||||
|
return True
|
||||||
|
except ValidationError as e:
|
||||||
|
messages.error(request, str(e.message))
|
||||||
return
|
return
|
||||||
if gc.testmode and not self.event.testmode:
|
|
||||||
messages.error(request, _("This gift card can only be used in test mode."))
|
|
||||||
return
|
|
||||||
if not gc.testmode and self.event.testmode:
|
|
||||||
messages.error(request, _("Only test gift cards can be used in test mode."))
|
|
||||||
return
|
|
||||||
if gc.expires and gc.expires < time_machine_now():
|
|
||||||
messages.error(request, _("This gift card is no longer valid."))
|
|
||||||
return
|
|
||||||
if gc.value <= Decimal("0.00"):
|
|
||||||
messages.error(request, _("All credit on this gift card has been used."))
|
|
||||||
return
|
|
||||||
|
|
||||||
for p in cs.get('payments', []):
|
|
||||||
if p['provider'] == self.identifier and p['info_data']['gift_card'] == gc.pk:
|
|
||||||
messages.error(request, _("This gift card is already used for your payment."))
|
|
||||||
return
|
|
||||||
|
|
||||||
add_payment_to_cart(
|
|
||||||
request,
|
|
||||||
self,
|
|
||||||
max_value=gc.value,
|
|
||||||
info_data={
|
|
||||||
'gift_card': gc.pk,
|
|
||||||
'gift_card_secret': gc.secret,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
except GiftCard.DoesNotExist:
|
except GiftCard.DoesNotExist:
|
||||||
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
|
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
|
||||||
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
|
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
|
||||||
|
|||||||
@@ -343,11 +343,13 @@ class CartManager:
|
|||||||
err = error_messages['some_subevent_not_started']
|
err = error_messages['some_subevent_not_started']
|
||||||
cp.addons.all().delete()
|
cp.addons.all().delete()
|
||||||
cp.delete()
|
cp.delete()
|
||||||
|
continue
|
||||||
|
|
||||||
if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
|
if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
|
||||||
err = error_messages['some_subevent_ended']
|
err = error_messages['some_subevent_ended']
|
||||||
cp.addons.all().delete()
|
cp.addons.all().delete()
|
||||||
cp.delete()
|
cp.delete()
|
||||||
|
continue
|
||||||
|
|
||||||
if cp.subevent:
|
if cp.subevent:
|
||||||
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||||
@@ -360,6 +362,7 @@ class CartManager:
|
|||||||
err = error_messages['some_subevent_ended']
|
err = error_messages['some_subevent_ended']
|
||||||
cp.addons.all().delete()
|
cp.addons.all().delete()
|
||||||
cp.delete()
|
cp.delete()
|
||||||
|
continue
|
||||||
return err
|
return err
|
||||||
|
|
||||||
def _update_subevents_cache(self, se_ids: List[int]):
|
def _update_subevents_cache(self, se_ids: List[int]):
|
||||||
@@ -1423,6 +1426,28 @@ class CartManager:
|
|||||||
raise CartError(err)
|
raise CartError(err)
|
||||||
|
|
||||||
|
|
||||||
|
def add_payment_to_cart_session(cart_session, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
||||||
|
"""
|
||||||
|
:param cart_session: The current cart session.
|
||||||
|
:param provider: The instance of your payment provider.
|
||||||
|
:param min_value: The minimum value this payment instrument supports, or ``None`` for unlimited.
|
||||||
|
:param max_value: The maximum value this payment instrument supports, or ``None`` for unlimited. Highly discouraged
|
||||||
|
to use for payment providers which charge a payment fee, as this can be very user-unfriendly if
|
||||||
|
users need a second payment method just for the payment fee of the first method.
|
||||||
|
:param info_data: A dictionary of information that will be passed through to the ``OrderPayment.info_data`` attribute.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
cart_session.setdefault('payments', [])
|
||||||
|
cart_session['payments'].append({
|
||||||
|
'id': str(uuid.uuid4()),
|
||||||
|
'provider': provider.identifier,
|
||||||
|
'multi_use_supported': provider.multi_use_supported,
|
||||||
|
'min_value': str(min_value) if min_value is not None else None,
|
||||||
|
'max_value': str(max_value) if max_value is not None else None,
|
||||||
|
'info_data': info_data or {},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
||||||
"""
|
"""
|
||||||
:param request: The current HTTP request context.
|
:param request: The current HTTP request context.
|
||||||
@@ -1437,16 +1462,7 @@ def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: D
|
|||||||
from pretix.presale.views.cart import cart_session
|
from pretix.presale.views.cart import cart_session
|
||||||
|
|
||||||
cs = cart_session(request)
|
cs = cart_session(request)
|
||||||
cs.setdefault('payments', [])
|
add_payment_to_cart_session(cs, provider, min_value, max_value, info_data)
|
||||||
|
|
||||||
cs['payments'].append({
|
|
||||||
'id': str(uuid.uuid4()),
|
|
||||||
'provider': provider.identifier,
|
|
||||||
'multi_use_supported': provider.multi_use_supported,
|
|
||||||
'min_value': str(min_value) if min_value is not None else None,
|
|
||||||
'max_value': str(max_value) if max_value is not None else None,
|
|
||||||
'info_data': info_data or {},
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def get_fees(event, request, total, invoice_address, payments, positions):
|
def get_fees(event, request, total, invoice_address, payments, positions):
|
||||||
@@ -1497,6 +1513,7 @@ def get_fees(event, request, total, invoice_address, payments, positions):
|
|||||||
value=payment_fee,
|
value=payment_fee,
|
||||||
tax_rate=payment_fee_tax.rate,
|
tax_rate=payment_fee_tax.rate,
|
||||||
tax_value=payment_fee_tax.tax,
|
tax_value=payment_fee_tax.tax,
|
||||||
|
tax_code=payment_fee_tax.code,
|
||||||
tax_rule=payment_fee_tax_rule
|
tax_rule=payment_fee_tax_rule
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ from pretix.base.models import (
|
|||||||
Checkin, CheckinList, Device, Event, Gate, Item, ItemVariation, Order,
|
Checkin, CheckinList, Device, Event, Gate, Item, ItemVariation, Order,
|
||||||
OrderPosition, QuestionOption,
|
OrderPosition, QuestionOption,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import checkin_created, order_placed, periodic_task
|
from pretix.base.signals import checkin_created, periodic_task
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.jsonlogic import Logic
|
from pretix.helpers.jsonlogic import Logic
|
||||||
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
||||||
@@ -1154,23 +1154,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(order_placed, dispatch_uid="legacy_autocheckin_order_placed")
|
|
||||||
def order_placed(sender, **kwargs):
|
|
||||||
order = kwargs['order']
|
|
||||||
event = sender
|
|
||||||
|
|
||||||
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels=order.sales_channel).prefetch_related(
|
|
||||||
'limit_products'))
|
|
||||||
if not cls:
|
|
||||||
return
|
|
||||||
for op in order.positions.all():
|
|
||||||
for cl in cls:
|
|
||||||
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
|
|
||||||
if not cl.subevent_id or cl.subevent_id == op.subevent_id:
|
|
||||||
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True, type=Checkin.TYPE_ENTRY)
|
|
||||||
checkin_created.send(event, checkin=ci)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(periodic_task, dispatch_uid="autocheckout_exit_all")
|
@receiver(periodic_task, dispatch_uid="autocheckout_exit_all")
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def process_exit_all(sender, **kwargs):
|
def process_exit_all(sender, **kwargs):
|
||||||
|
|||||||
@@ -271,7 +271,9 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
|||||||
event_date_from=p.subevent.date_from if invoice.event.has_subevents else invoice.event.date_from,
|
event_date_from=p.subevent.date_from if invoice.event.has_subevents else invoice.event.date_from,
|
||||||
event_date_to=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to,
|
event_date_to=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to,
|
||||||
event_location=location if invoice.event.settings.invoice_event_location else None,
|
event_location=location if invoice.event.settings.invoice_event_location else None,
|
||||||
tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
|
tax_rate=p.tax_rate,
|
||||||
|
tax_code=p.tax_code,
|
||||||
|
tax_name=p.tax_rule.name if p.tax_rule else ''
|
||||||
)
|
)
|
||||||
|
|
||||||
if p.tax_rule and p.tax_rule.is_reverse_charge(ia) and p.price and not p.tax_value:
|
if p.tax_rule and p.tax_rule.is_reverse_charge(ia) and p.price and not p.tax_value:
|
||||||
@@ -305,6 +307,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
|||||||
),
|
),
|
||||||
tax_value=fee.tax_value,
|
tax_value=fee.tax_value,
|
||||||
tax_rate=fee.tax_rate,
|
tax_rate=fee.tax_rate,
|
||||||
|
tax_code=fee.tax_code,
|
||||||
tax_name=fee.tax_rule.name if fee.tax_rule else '',
|
tax_name=fee.tax_rule.name if fee.tax_rule else '',
|
||||||
fee_type=fee.fee_type,
|
fee_type=fee.fee_type,
|
||||||
fee_internal_type=fee.internal_type or None,
|
fee_internal_type=fee.internal_type or None,
|
||||||
@@ -491,13 +494,13 @@ def build_preview_invoice_pdf(event):
|
|||||||
InvoiceLine.objects.create(
|
InvoiceLine.objects.create(
|
||||||
invoice=invoice, description=_("Sample product {}").format(i + 1),
|
invoice=invoice, description=_("Sample product {}").format(i + 1),
|
||||||
gross_value=tax.gross, tax_value=tax.tax,
|
gross_value=tax.gross, tax_value=tax.tax,
|
||||||
tax_rate=tax.rate, tax_name=tax.name
|
tax_rate=tax.rate, tax_name=tax.name, tax_code=tax.code,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
InvoiceLine.objects.create(
|
InvoiceLine.objects.create(
|
||||||
invoice=invoice, description=_("Sample product A"),
|
invoice=invoice, description=_("Sample product A"),
|
||||||
gross_value=100, tax_value=0, tax_rate=0
|
gross_value=100, tax_value=0, tax_rate=0, tax_code=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return event.invoice_renderer.generate(invoice)
|
return event.invoice_renderer.generate(invoice)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ from pretix.base.services.tasks import TransactionAwareTask
|
|||||||
from pretix.base.services.tickets import get_tickets_for_order
|
from pretix.base.services.tickets import get_tickets_for_order
|
||||||
from pretix.base.signals import email_filter, global_email_filter
|
from pretix.base.signals import email_filter, global_email_filter
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers.format import format_map
|
from pretix.helpers.format import SafeFormatter, format_map
|
||||||
from pretix.helpers.hierarkey import clean_filename
|
from pretix.helpers.hierarkey import clean_filename
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
from pretix.presale.ical import get_private_icals
|
from pretix.presale.ical import get_private_icals
|
||||||
@@ -311,11 +311,17 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
try:
|
try:
|
||||||
if plain_text_only:
|
if plain_text_only:
|
||||||
body_html = None
|
body_html = None
|
||||||
|
elif 'context' in inspect.signature(renderer.render).parameters:
|
||||||
|
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
|
||||||
elif 'position' in inspect.signature(renderer.render).parameters:
|
elif 'position' in inspect.signature(renderer.render).parameters:
|
||||||
|
# Backwards compatibility
|
||||||
|
warnings.warn('Email renderer called without context argument because context argument is not '
|
||||||
|
'supported.',
|
||||||
|
DeprecationWarning)
|
||||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
||||||
else:
|
else:
|
||||||
# Backwards compatibility
|
# Backwards compatibility
|
||||||
warnings.warn('E-mail renderer called without position argument because position argument is not '
|
warnings.warn('Email renderer called without position argument because position argument is not '
|
||||||
'supported.',
|
'supported.',
|
||||||
DeprecationWarning)
|
DeprecationWarning)
|
||||||
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
||||||
@@ -323,6 +329,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
logger.exception('Could not render HTML body')
|
logger.exception('Could not render HTML body')
|
||||||
body_html = None
|
body_html = None
|
||||||
|
|
||||||
|
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||||
|
|
||||||
send_task = mail_send_task.si(
|
send_task = mail_send_task.si(
|
||||||
to=[email] if isinstance(email, str) else list(email),
|
to=[email] if isinstance(email, str) else list(email),
|
||||||
cc=cc,
|
cc=cc,
|
||||||
@@ -655,7 +663,7 @@ def render_mail(template, context):
|
|||||||
if isinstance(template, LazyI18nString):
|
if isinstance(template, LazyI18nString):
|
||||||
body = str(template)
|
body = str(template)
|
||||||
if context:
|
if context:
|
||||||
body = format_map(body, context)
|
body = format_map(body, context, mode=SafeFormatter.MODE_IGNORE_RICH)
|
||||||
else:
|
else:
|
||||||
tpl = get_template(template)
|
tpl = get_template(template)
|
||||||
body = tpl.render(context)
|
body = tpl.render(context)
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
|
|||||||
c.assign(record.get(c.identifier), order, position, order._address)
|
c.assign(record.get(c.identifier), order, position, order._address)
|
||||||
|
|
||||||
if position.seat is not None:
|
if position.seat is not None:
|
||||||
lock_seats.append(position.seat)
|
lock_seats.append((order.sales_channel, position.seat))
|
||||||
except (ValidationError, ImportError) as e:
|
except (ValidationError, ImportError) as e:
|
||||||
raise DataImportError(
|
raise DataImportError(
|
||||||
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
|
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
|
||||||
@@ -128,9 +128,9 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# We don't support vouchers, quotas, or memberships here, so we only need to lock if seats are in use
|
# We don't support vouchers, quotas, or memberships here, so we only need to lock if seats are in use
|
||||||
if lock_seats:
|
if lock_seats:
|
||||||
lock_objects(lock_seats, shared_lock_objects=[event])
|
lock_objects([s for c, s in lock_seats], shared_lock_objects=[event])
|
||||||
for s in lock_seats:
|
for c, s in lock_seats:
|
||||||
if not s.is_available():
|
if not s.is_available(sales_channel=c):
|
||||||
raise DataImportError(_('The seat you selected has already been taken. Please select a different seat.'))
|
raise DataImportError(_('The seat you selected has already been taken. Please select a different seat.'))
|
||||||
|
|
||||||
save_transactions = []
|
save_transactions = []
|
||||||
|
|||||||
@@ -1721,16 +1721,17 @@ class OrderChangeManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
new_rate = tax_rule.tax_rate_for(ia)
|
new_rate = tax_rule.tax_rate_for(ia)
|
||||||
|
new_code = tax_rule.tax_code_for(ia)
|
||||||
except TaxRule.SaleNotAllowed:
|
except TaxRule.SaleNotAllowed:
|
||||||
raise OrderError(error_messages['tax_rule_country_blocked'])
|
raise OrderError(error_messages['tax_rule_country_blocked'])
|
||||||
# We use override_tax_rate to make sure .tax() doesn't get clever and re-adjusts the pricing itself
|
# We use override_tax_rate to make sure .tax() doesn't get clever and re-adjusts the pricing itself
|
||||||
if new_rate != pos.tax_rate:
|
if new_rate != pos.tax_rate or new_code != pos.tax_code:
|
||||||
if keep == 'net':
|
if keep == 'net':
|
||||||
new_tax = tax_rule.tax(pos.price - pos.tax_value, base_price_is='net', currency=self.event.currency,
|
new_tax = tax_rule.tax(pos.price - pos.tax_value, base_price_is='net', currency=self.event.currency,
|
||||||
override_tax_rate=new_rate)
|
override_tax_rate=new_rate, override_tax_code=new_code)
|
||||||
else:
|
else:
|
||||||
new_tax = tax_rule.tax(pos.price, base_price_is='gross', currency=self.event.currency,
|
new_tax = tax_rule.tax(pos.price, base_price_is='gross', currency=self.event.currency,
|
||||||
override_tax_rate=new_rate)
|
override_tax_rate=new_rate, override_tax_code=new_code)
|
||||||
self._totaldiff += new_tax.gross - pos.price
|
self._totaldiff += new_tax.gross - pos.price
|
||||||
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
|
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
|
||||||
self._invoice_dirty = True
|
self._invoice_dirty = True
|
||||||
@@ -2304,6 +2305,7 @@ class OrderChangeManager:
|
|||||||
op.position.price = op.price.gross
|
op.position.price = op.price.gross
|
||||||
op.position.tax_rate = op.price.rate
|
op.position.tax_rate = op.price.rate
|
||||||
op.position.tax_value = op.price.tax
|
op.position.tax_value = op.price.tax
|
||||||
|
op.position.tax_code = op.price.code
|
||||||
op.position.save()
|
op.position.save()
|
||||||
elif isinstance(op, self.TaxRuleOperation):
|
elif isinstance(op, self.TaxRuleOperation):
|
||||||
if isinstance(op.position, OrderPosition):
|
if isinstance(op.position, OrderPosition):
|
||||||
@@ -2400,7 +2402,7 @@ class OrderChangeManager:
|
|||||||
elif isinstance(op, self.AddOperation):
|
elif isinstance(op, self.AddOperation):
|
||||||
pos = OrderPosition.objects.create(
|
pos = OrderPosition.objects.create(
|
||||||
item=op.item, variation=op.variation, addon_to=op.addon_to,
|
item=op.item, variation=op.variation, addon_to=op.addon_to,
|
||||||
price=op.price.gross, order=self.order, tax_rate=op.price.rate,
|
price=op.price.gross, order=self.order, tax_rate=op.price.rate, tax_code=op.price.code,
|
||||||
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
|
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
|
||||||
positionid=nextposid, subevent=op.subevent, seat=op.seat,
|
positionid=nextposid, subevent=op.subevent, seat=op.seat,
|
||||||
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
|
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
|
||||||
@@ -2423,6 +2425,8 @@ class OrderChangeManager:
|
|||||||
elif isinstance(op, self.SplitOperation):
|
elif isinstance(op, self.SplitOperation):
|
||||||
split_positions.append(op.position)
|
split_positions.append(op.position)
|
||||||
elif isinstance(op, self.RegenerateSecretOperation):
|
elif isinstance(op, self.RegenerateSecretOperation):
|
||||||
|
op.position.web_secret = generate_secret()
|
||||||
|
op.position.save(update_fields=["web_secret"])
|
||||||
assign_ticket_secret(
|
assign_ticket_secret(
|
||||||
event=self.event, position=op.position, force_invalidate=True, save=True
|
event=self.event, position=op.position, force_invalidate=True, save=True
|
||||||
)
|
)
|
||||||
@@ -2529,6 +2533,7 @@ class OrderChangeManager:
|
|||||||
'new_order': split_order.code,
|
'new_order': split_order.code,
|
||||||
})
|
})
|
||||||
op.order = split_order
|
op.order = split_order
|
||||||
|
op.web_secret = generate_secret()
|
||||||
assign_ticket_secret(
|
assign_ticket_secret(
|
||||||
self.event, position=op, force_invalidate=True,
|
self.event, position=op, force_invalidate=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
|
from django.utils.html import escape
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -39,7 +40,8 @@ from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
|
|||||||
from pretix.base.signals import (
|
from pretix.base.signals import (
|
||||||
register_mail_placeholders, register_text_placeholders,
|
register_mail_placeholders, register_text_placeholders,
|
||||||
)
|
)
|
||||||
from pretix.helpers.format import SafeFormatter
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||||
|
from pretix.helpers.format import PlainHtmlAlternativeString, SafeFormatter
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.base.services.placeholders')
|
logger = logging.getLogger('pretix.base.services.placeholders')
|
||||||
|
|
||||||
@@ -107,6 +109,91 @@ class SimpleFunctionalTextPlaceholder(BaseTextPlaceholder):
|
|||||||
return self._sample
|
return self._sample
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRichTextPlaceholder(BaseTextPlaceholder):
|
||||||
|
"""
|
||||||
|
This is the base class for all placeholders which can render either to plain text
|
||||||
|
or to a rich HTML element.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, identifier, args):
|
||||||
|
self._identifier = identifier
|
||||||
|
self._args = args
|
||||||
|
|
||||||
|
@property
|
||||||
|
def identifier(self):
|
||||||
|
return self._identifier
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required_context(self):
|
||||||
|
return self._args
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_block(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
return PlainHtmlAlternativeString(
|
||||||
|
self.render_plain(**{k: context[k] for k in self._args}),
|
||||||
|
self.render_html(**{k: context[k] for k in self._args}),
|
||||||
|
self.is_block,
|
||||||
|
)
|
||||||
|
|
||||||
|
def render_html(self, **kwargs):
|
||||||
|
"""
|
||||||
|
HTML rendering of the placeholder. Should return "safe" HTML, i.e. everything needs to be
|
||||||
|
escaped.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def render_plain(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Plain text rendering of the placeholder.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def render_sample(self, event):
|
||||||
|
return PlainHtmlAlternativeString(
|
||||||
|
self.render_sample_plain(event=event),
|
||||||
|
self.render_sample_html(event=event),
|
||||||
|
self.is_block,
|
||||||
|
)
|
||||||
|
|
||||||
|
def render_sample_html(self, event):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def render_sample_plain(self, event):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleButtonPlaceholder(BaseRichTextPlaceholder):
|
||||||
|
def __init__(self, identifier, args, url_func, text_func, sample_url_func, sample_text_func):
|
||||||
|
super().__init__(identifier, args)
|
||||||
|
self._url_func = url_func
|
||||||
|
self._text_func = text_func
|
||||||
|
self._sample_url_func = sample_url_func
|
||||||
|
self._sample_text_func = sample_text_func
|
||||||
|
|
||||||
|
def render_html(self, **context):
|
||||||
|
text = self._text_func(**{k: context[k] for k in self._args})
|
||||||
|
url = self._url_func(**{k: context[k] for k in self._args})
|
||||||
|
return f'<a href="{url}" class="button">{escape(text)}</a>'
|
||||||
|
|
||||||
|
def render_plain(self, **context):
|
||||||
|
text = self._text_func(**{k: context[k] for k in self._args})
|
||||||
|
url = self._url_func(**{k: context[k] for k in self._args})
|
||||||
|
return f'{text}: {url}'
|
||||||
|
|
||||||
|
def render_sample_html(self, event):
|
||||||
|
text = self._sample_text_func(event)
|
||||||
|
url = self._sample_url_func(event)
|
||||||
|
return f'<a href="{url}" class="button">{escape(text)}</a>'
|
||||||
|
|
||||||
|
def render_sample_plain(self, event):
|
||||||
|
text = self._sample_text_func(event)
|
||||||
|
url = self._sample_url_func(event)
|
||||||
|
return f'{text}: {url}'
|
||||||
|
|
||||||
|
|
||||||
class PlaceholderContext(SafeFormatter):
|
class PlaceholderContext(SafeFormatter):
|
||||||
"""
|
"""
|
||||||
Holds the contextual arguments and corresponding list of available placeholders for formatting
|
Holds the contextual arguments and corresponding list of available placeholders for formatting
|
||||||
@@ -209,13 +296,24 @@ def get_best_name(position_or_address, parts=False):
|
|||||||
def base_placeholders(sender, **kwargs):
|
def base_placeholders(sender, **kwargs):
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
|
def _event_sample(event):
|
||||||
|
if event.has_subevents:
|
||||||
|
se = event.subevents.first()
|
||||||
|
if se:
|
||||||
|
return se.name
|
||||||
|
return event.name
|
||||||
|
|
||||||
ph = [
|
ph = [
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'event', ['event'], lambda event: event.name, lambda event: event.name
|
'event', ['event'], lambda event: event.name, lambda event: event.name
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
|
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
|
||||||
lambda event_or_subevent: event_or_subevent.name
|
_event_sample,
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'event_series_name', ['event', 'event_or_subevent'], lambda event, event_or_subevent: event.name,
|
||||||
|
lambda event: event.name
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
||||||
@@ -273,6 +371,27 @@ def base_placeholders(sender, **kwargs):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SimpleButtonPlaceholder(
|
||||||
|
'url_button', ['order', 'event'],
|
||||||
|
url_func=lambda order, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.open', kwargs={
|
||||||
|
'order': order.code,
|
||||||
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_secret()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
text_func=lambda order, event: _("View order details"),
|
||||||
|
sample_url_func=lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.open', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
'hash': '98kusd8ofsj8dnkd'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
sample_text_func=lambda event: _("View order details"),
|
||||||
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
@@ -337,6 +456,27 @@ def base_placeholders(sender, **kwargs):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SimpleButtonPlaceholder(
|
||||||
|
'url_button', ['event', 'position'],
|
||||||
|
url_func=lambda event, position: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position', kwargs={
|
||||||
|
'order': position.order.code,
|
||||||
|
'secret': position.web_secret,
|
||||||
|
'position': position.positionid
|
||||||
|
}
|
||||||
|
),
|
||||||
|
text_func=lambda event, position: _("View registration details"),
|
||||||
|
sample_url_func=lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
'position': '123'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
sample_text_func=lambda event: _("View registration details"),
|
||||||
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
@@ -592,8 +732,8 @@ def base_placeholders(sender, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
class FormPlaceholderMixin:
|
class FormPlaceholderMixin:
|
||||||
def _set_field_placeholders(self, fn, base_parameters):
|
def _set_field_placeholders(self, fn, base_parameters, rich=False):
|
||||||
placeholders = get_available_placeholders(self.event, base_parameters)
|
placeholders = get_available_placeholders(self.event, base_parameters, rich=rich)
|
||||||
ht = format_placeholders_help_text(placeholders, self.event)
|
ht = format_placeholders_help_text(placeholders, self.event)
|
||||||
if self.fields[fn].help_text:
|
if self.fields[fn].help_text:
|
||||||
self.fields[fn].help_text += ' ' + str(ht)
|
self.fields[fn].help_text += ' ' + str(ht)
|
||||||
@@ -604,7 +744,7 @@ class FormPlaceholderMixin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_available_placeholders(event, base_parameters):
|
def get_available_placeholders(event, base_parameters, rich=False):
|
||||||
if 'order' in base_parameters:
|
if 'order' in base_parameters:
|
||||||
base_parameters.append('invoice_address')
|
base_parameters.append('invoice_address')
|
||||||
base_parameters.append('position_or_address')
|
base_parameters.append('position_or_address')
|
||||||
@@ -613,6 +753,35 @@ def get_available_placeholders(event, base_parameters):
|
|||||||
if not isinstance(val, (list, tuple)):
|
if not isinstance(val, (list, tuple)):
|
||||||
val = [val]
|
val = [val]
|
||||||
for v in val:
|
for v in val:
|
||||||
|
if isinstance(v, BaseRichTextPlaceholder) and not rich:
|
||||||
|
continue
|
||||||
if all(rp in base_parameters for rp in v.required_context):
|
if all(rp in base_parameters for rp in v.required_context):
|
||||||
params[v.identifier] = v
|
params[v.identifier] = v
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def get_sample_context(event, context_parameters, rich=True):
|
||||||
|
context_dict = {}
|
||||||
|
lbl = _('This value will be replaced based on dynamic parameters.')
|
||||||
|
for k, v in get_available_placeholders(event, context_parameters, rich=rich).items():
|
||||||
|
sample = v.render_sample(event)
|
||||||
|
if isinstance(sample, PlainHtmlAlternativeString):
|
||||||
|
context_dict[k] = PlainHtmlAlternativeString(
|
||||||
|
sample.plain,
|
||||||
|
'<{el} class="placeholder placeholder-html" title="{title}">{html}</{el}>'.format(
|
||||||
|
el='div' if sample.is_block else 'span',
|
||||||
|
title=lbl,
|
||||||
|
html=sample.html,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif str(sample).strip().startswith('* ') or str(sample).startswith(' '):
|
||||||
|
context_dict[k] = '<div class="placeholder" title="{}">{}</div>'.format(
|
||||||
|
lbl,
|
||||||
|
markdown_compile_email(str(sample))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||||
|
lbl,
|
||||||
|
escape(sample)
|
||||||
|
)
|
||||||
|
return context_dict
|
||||||
|
|||||||
@@ -91,9 +91,11 @@ def get_price(item: Item, variation: ItemVariation = None,
|
|||||||
|
|
||||||
if custom_price_is_net:
|
if custom_price_is_net:
|
||||||
price = tax_rule.tax(max(custom_price, price.net), base_price_is='net', override_tax_rate=price.rate,
|
price = tax_rule.tax(max(custom_price, price.net), base_price_is='net', override_tax_rate=price.rate,
|
||||||
|
override_tax_code=price.code,
|
||||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||||
else:
|
else:
|
||||||
price = tax_rule.tax(max(custom_price, price.gross), base_price_is='gross', override_tax_rate=price.rate,
|
price = tax_rule.tax(max(custom_price, price.gross), base_price_is='gross', override_tax_rate=price.rate,
|
||||||
|
override_tax_code=price.code,
|
||||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||||
else:
|
else:
|
||||||
price = tax_rule.tax(price, invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
price = tax_rule.tax(price, invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||||
@@ -146,10 +148,12 @@ def get_line_price(price_after_voucher: Decimal, custom_price_input: Decimal, cu
|
|||||||
|
|
||||||
if custom_price_input_is_net:
|
if custom_price_input_is_net:
|
||||||
price = tax_rule.tax(max(custom_price_input, price.net), base_price_is='net', override_tax_rate=price.rate,
|
price = tax_rule.tax(max(custom_price_input, price.net), base_price_is='net', override_tax_rate=price.rate,
|
||||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
override_tax_code=price.code, invoice_address=invoice_address,
|
||||||
|
subtract_from_gross=bundled_sum)
|
||||||
else:
|
else:
|
||||||
price = tax_rule.tax(max(custom_price_input, price.gross), base_price_is='gross', override_tax_rate=price.rate,
|
price = tax_rule.tax(max(custom_price_input, price.gross), base_price_is='gross', override_tax_rate=price.rate,
|
||||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
override_tax_code=price.code, invoice_address=invoice_address,
|
||||||
|
subtract_from_gross=bundled_sum)
|
||||||
else:
|
else:
|
||||||
price = tax_rule.tax(price_after_voucher, invoice_address=invoice_address, subtract_from_gross=bundled_sum,
|
price = tax_rule.tax(price_after_voucher, invoice_address=invoice_address, subtract_from_gross=bundled_sum,
|
||||||
base_price_is='gross' if is_bundled else 'auto')
|
base_price_is='gross' if is_bundled else 'auto')
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
@@ -97,9 +98,9 @@ def preview(event: int, provider: str):
|
|||||||
event = Event.objects.get(id=event)
|
event = Event.objects.get(id=event)
|
||||||
|
|
||||||
with rolledback_transaction(), language(event.settings.locale, event.settings.region):
|
with rolledback_transaction(), language(event.settings.locale, event.settings.region):
|
||||||
item = event.items.create(name=_("Sample product"), default_price=42.23,
|
item = event.items.create(name=_("Sample product"), default_price=Decimal('42.23'),
|
||||||
description=_("Sample product description"))
|
description=_("Sample product description"))
|
||||||
item2 = event.items.create(name=_("Sample workshop"), default_price=23.40)
|
item2 = event.items.create(name=_("Sample workshop"), default_price=Decimal('23.40'))
|
||||||
|
|
||||||
from pretix.base.models import Order
|
from pretix.base.models import Order
|
||||||
order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ from django.utils.translation import (
|
|||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from hierarkey.models import GlobalSettingsBase, Hierarkey
|
from hierarkey.models import GlobalSettingsBase, Hierarkey
|
||||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||||
|
from i18nfield.rest_framework import I18nField
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
from phonenumbers import PhoneNumber, parse
|
from phonenumbers import PhoneNumber, parse
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
@@ -63,7 +64,7 @@ from rest_framework import serializers
|
|||||||
from pretix.api.serializers.fields import (
|
from pretix.api.serializers.fields import (
|
||||||
ListMultipleChoiceField, UploadedFileField,
|
ListMultipleChoiceField, UploadedFileField,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.i18n import I18nField, I18nURLField
|
from pretix.api.serializers.i18n import I18nURLField
|
||||||
from pretix.base.forms import I18nMarkdownTextarea, I18nURLFormField
|
from pretix.base.forms import I18nMarkdownTextarea, I18nURLFormField
|
||||||
from pretix.base.models.tax import VAT_ID_COUNTRIES, TaxRule
|
from pretix.base.models.tax import VAT_ID_COUNTRIES, TaxRule
|
||||||
from pretix.base.reldate import (
|
from pretix.base.reldate import (
|
||||||
@@ -550,7 +551,7 @@ DEFAULTS = {
|
|||||||
'serializer_class': serializers.BooleanField,
|
'serializer_class': serializers.BooleanField,
|
||||||
'type': bool,
|
'type': bool,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Require a business addresses"),
|
label=_("Require a business address"),
|
||||||
help_text=_('This will require users to enter a company name.'),
|
help_text=_('This will require users to enter a company name.'),
|
||||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_required'}),
|
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_required'}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -287,9 +287,9 @@ class PhoneNumberShredder(BaseDataShredder):
|
|||||||
|
|
||||||
|
|
||||||
class EmailAddressShredder(BaseDataShredder):
|
class EmailAddressShredder(BaseDataShredder):
|
||||||
verbose_name = _('E-mails')
|
verbose_name = _('Emails')
|
||||||
identifier = 'order_emails'
|
identifier = 'order_emails'
|
||||||
description = _('This will remove all e-mail addresses from orders and attendees, as well as logged email '
|
description = _('This will remove all email addresses from orders and attendees, as well as logged email '
|
||||||
'contents. This will also remove the association to customer accounts.')
|
'contents. This will also remove the association to customer accounts.')
|
||||||
|
|
||||||
def generate_files(self) -> List[Tuple[str, str, str]]:
|
def generate_files(self) -> List[Tuple[str, str, str]]:
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ validate_cart_addons = EventPluginSignal()
|
|||||||
Arguments: ``addons``, ``base_position``, ``iao``
|
Arguments: ``addons``, ``base_position``, ``iao``
|
||||||
|
|
||||||
This signal is sent when a user tries to select a combination of addons. In contrast to
|
This signal is sent when a user tries to select a combination of addons. In contrast to
|
||||||
``validate_cart``, this is executed before the cart is actually modified. You are passed
|
``validate_cart``, this is executed before the cart is actually modified. You are passed
|
||||||
an argument ``addons`` containing a dict of ``(item, variation or None) → count`` tuples as well
|
an argument ``addons`` containing a dict of ``(item, variation or None) → count`` tuples as well
|
||||||
as the ``ItemAddOn`` object as the argument ``iao`` and the base cart position as
|
as the ``ItemAddOn`` object as the argument ``iao`` and the base cart position as
|
||||||
``base_position``.
|
``base_position``.
|
||||||
|
|||||||
@@ -131,6 +131,9 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
.content table td.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
a.button {
|
a.button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -178,6 +181,9 @@
|
|||||||
pre, pre code {
|
pre, pre code {
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
|
.text-right, .content table td.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
{% if rtl %}
|
{% if rtl %}
|
||||||
body {
|
body {
|
||||||
@@ -186,6 +192,9 @@
|
|||||||
.content {
|
.content {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
.text-right, .content table td.text-right {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% block addcss %}{% endblock %}
|
{% block addcss %}{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from django import template
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def icon(key, *args, **kwargs):
|
||||||
|
return format_html(
|
||||||
|
'<span class="fa fa-{} {}" aria-hidden="true"></span>',
|
||||||
|
key,
|
||||||
|
kwargs["class"] if "class" in kwargs else "",
|
||||||
|
)
|
||||||
@@ -52,12 +52,12 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
|
|||||||
# would make the numbers incorrect. If this branch executes, it's likely a bug in
|
# would make the numbers incorrect. If this branch executes, it's likely a bug in
|
||||||
# pretix, but we won't show wrong numbers!
|
# pretix, but we won't show wrong numbers!
|
||||||
if hide_currency:
|
if hide_currency:
|
||||||
return floatformat(value, 2)
|
return floatformat(value, "2g")
|
||||||
else:
|
else:
|
||||||
return '{} {}'.format(arg, floatformat(value, 2))
|
return '{} {}'.format(arg, floatformat(value, "2g"))
|
||||||
|
|
||||||
if hide_currency:
|
if hide_currency:
|
||||||
return floatformat(value, places)
|
return floatformat(value, f"{places}g")
|
||||||
|
|
||||||
locale_parts = translation.get_language().split("-", 1)
|
locale_parts = translation.get_language().split("-", 1)
|
||||||
locale = locale_parts[0]
|
locale = locale_parts[0]
|
||||||
@@ -70,7 +70,7 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
|
|||||||
try:
|
try:
|
||||||
return format_currency(value, arg, locale=locale)
|
return format_currency(value, arg, locale=locale)
|
||||||
except:
|
except:
|
||||||
return '{} {}'.format(arg, floatformat(value, places))
|
return '{} {}'.format(arg, floatformat(value, f"{places}g"))
|
||||||
|
|
||||||
|
|
||||||
@register.filter("money_numberfield")
|
@register.filter("money_numberfield")
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ from tlds import tld_set
|
|||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
ALLOWED_TAGS_SNIPPET = [
|
ALLOWED_TAGS_SNIPPET = {
|
||||||
'a',
|
'a',
|
||||||
'abbr',
|
'abbr',
|
||||||
'acronym',
|
'acronym',
|
||||||
@@ -68,8 +68,8 @@ ALLOWED_TAGS_SNIPPET = [
|
|||||||
'strike',
|
'strike',
|
||||||
's',
|
's',
|
||||||
# Update doc/user/markdown.rst if you change this!
|
# Update doc/user/markdown.rst if you change this!
|
||||||
]
|
}
|
||||||
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
|
ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET | {
|
||||||
'blockquote',
|
'blockquote',
|
||||||
'li',
|
'li',
|
||||||
'ol',
|
'ol',
|
||||||
@@ -91,7 +91,7 @@ ALLOWED_TAGS = ALLOWED_TAGS_SNIPPET + [
|
|||||||
'h6',
|
'h6',
|
||||||
'pre',
|
'pre',
|
||||||
# Update doc/user/markdown.rst if you change this!
|
# Update doc/user/markdown.rst if you change this!
|
||||||
]
|
}
|
||||||
|
|
||||||
ALLOWED_ATTRIBUTES = {
|
ALLOWED_ATTRIBUTES = {
|
||||||
'a': ['href', 'title', 'class'],
|
'a': ['href', 'title', 'class'],
|
||||||
@@ -106,7 +106,7 @@ ALLOWED_ATTRIBUTES = {
|
|||||||
# Update doc/user/markdown.rst if you change this!
|
# Update doc/user/markdown.rst if you change this!
|
||||||
}
|
}
|
||||||
|
|
||||||
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto', 'tel']
|
ALLOWED_PROTOCOLS = {'http', 'https', 'mailto', 'tel'}
|
||||||
|
|
||||||
URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, reverse=True)))
|
URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, reverse=True)))
|
||||||
|
|
||||||
@@ -211,9 +211,9 @@ class CleanPostprocessor(Postprocessor):
|
|||||||
def run(self, text):
|
def run(self, text):
|
||||||
return bleach.clean(
|
return bleach.clean(
|
||||||
text,
|
text,
|
||||||
tags=self.tags,
|
tags=set(self.tags),
|
||||||
attributes=self.attributes,
|
attributes=self.attributes,
|
||||||
protocols=self.protocols,
|
protocols=set(self.protocols),
|
||||||
strip=self.strip
|
strip=self.strip
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -305,10 +305,11 @@ def markdown_compile_email(source, allowed_tags=ALLOWED_TAGS, allowed_attributes
|
|||||||
source,
|
source,
|
||||||
extensions=[
|
extensions=[
|
||||||
'markdown.extensions.sane_lists',
|
'markdown.extensions.sane_lists',
|
||||||
|
'markdown.extensions.tables',
|
||||||
EmailNl2BrExtension(),
|
EmailNl2BrExtension(),
|
||||||
LinkifyAndCleanExtension(
|
LinkifyAndCleanExtension(
|
||||||
linker,
|
linker,
|
||||||
tags=allowed_tags,
|
tags=set(allowed_tags),
|
||||||
attributes=allowed_attributes,
|
attributes=allowed_attributes,
|
||||||
protocols=ALLOWED_PROTOCOLS,
|
protocols=ALLOWED_PROTOCOLS,
|
||||||
strip=False,
|
strip=False,
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from django import template
|
||||||
|
from django.utils.html import format_html, mark_safe
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def textbubble(type, *args, **kwargs):
|
||||||
|
return format_html(
|
||||||
|
'<span class="textbubble-{}">{}',
|
||||||
|
type or "info",
|
||||||
|
"" if "icon" not in kwargs else format_html(
|
||||||
|
'<i class="fa fa-{}" aria-hidden="true"></i> ',
|
||||||
|
kwargs["icon"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def endtextbubble():
|
||||||
|
return mark_safe('</span>')
|
||||||
@@ -22,16 +22,30 @@
|
|||||||
import pycountry
|
import pycountry
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
|
||||||
|
from pretix.base.addressvalidation import (
|
||||||
|
COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED,
|
||||||
|
)
|
||||||
|
from pretix.base.models.tax import VAT_ID_COUNTRIES
|
||||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||||
|
|
||||||
|
|
||||||
def states(request):
|
def states(request):
|
||||||
cc = request.GET.get("country", "DE")
|
cc = request.GET.get("country", "DE")
|
||||||
|
info = {
|
||||||
|
'street': {'required': True},
|
||||||
|
'zipcode': {'required': cc in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED},
|
||||||
|
'city': {'required': cc in COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED},
|
||||||
|
'state': {'visible': cc in COUNTRIES_WITH_STATE_IN_ADDRESS, 'required': cc in COUNTRIES_WITH_STATE_IN_ADDRESS},
|
||||||
|
'vat_id': {'visible': cc in VAT_ID_COUNTRIES, 'required': False},
|
||||||
|
}
|
||||||
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
return JsonResponse({'data': []})
|
return JsonResponse({'data': [], **info, })
|
||||||
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc]
|
||||||
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
||||||
return JsonResponse({'data': [
|
return JsonResponse({
|
||||||
{'name': s.name, 'code': s.code[3:]}
|
'data': [
|
||||||
for s in sorted(statelist, key=lambda s: s.name)
|
{'name': s.name, 'code': s.code[3:]}
|
||||||
]})
|
for s in sorted(statelist, key=lambda s: s.name)
|
||||||
|
],
|
||||||
|
**info,
|
||||||
|
})
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ from django_scopes.forms import (
|
|||||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||||
from pretix.base.models import Gate
|
from pretix.base.models import Gate
|
||||||
from pretix.base.models.checkin import Checkin, CheckinList
|
from pretix.base.models.checkin import Checkin, CheckinList
|
||||||
from pretix.control.forms import (
|
from pretix.control.forms import ItemMultipleChoiceField
|
||||||
ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
|
|
||||||
)
|
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
|
|
||||||
|
|
||||||
@@ -67,10 +65,6 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
kwargs.pop('locales', None)
|
kwargs.pop('locales', None)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.fields['limit_products'].queryset = self.event.items.all()
|
self.fields['limit_products'].queryset = self.event.items.all()
|
||||||
self.fields['auto_checkin_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
|
||||||
self.fields['auto_checkin_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(
|
|
||||||
self.event, choices=self.fields['auto_checkin_sales_channels'].widget.choices
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.event.organizer.gates.exists():
|
if not self.event.organizer.gates.exists():
|
||||||
del self.fields['gates']
|
del self.fields['gates']
|
||||||
@@ -102,7 +96,6 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
'limit_products',
|
'limit_products',
|
||||||
'subevent',
|
'subevent',
|
||||||
'include_pending',
|
'include_pending',
|
||||||
'auto_checkin_sales_channels',
|
|
||||||
'allow_multiple_entries',
|
'allow_multiple_entries',
|
||||||
'allow_entry_after_exit',
|
'allow_entry_after_exit',
|
||||||
'rules',
|
'rules',
|
||||||
@@ -125,7 +118,6 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
'limit_products': ItemMultipleChoiceField,
|
'limit_products': ItemMultipleChoiceField,
|
||||||
'gates': SafeModelMultipleChoiceField,
|
'gates': SafeModelMultipleChoiceField,
|
||||||
'subevent': SafeModelChoiceField,
|
'subevent': SafeModelChoiceField,
|
||||||
'auto_checkin_sales_channels': SafeModelMultipleChoiceField,
|
|
||||||
'exit_all_at': NextTimeField,
|
'exit_all_at': NextTimeField,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import pycountry
|
import pycountry
|
||||||
@@ -63,6 +63,7 @@ from pretix.base.forms import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models import Event, Organizer, TaxRule, Team
|
from pretix.base.models import Event, Organizer, TaxRule, Team
|
||||||
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
||||||
|
from pretix.base.models.tax import TAX_CODE_LISTS
|
||||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||||
from pretix.base.services.placeholders import FormPlaceholderMixin
|
from pretix.base.services.placeholders import FormPlaceholderMixin
|
||||||
from pretix.base.settings import (
|
from pretix.base.settings import (
|
||||||
@@ -76,8 +77,10 @@ from pretix.control.forms import (
|
|||||||
)
|
)
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.multidomain.models import KnownDomain
|
from pretix.multidomain.models import AlternativeDomainAssignment, KnownDomain
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
from pretix.multidomain.urlreverse import (
|
||||||
|
build_absolute_uri, get_organizer_domain,
|
||||||
|
)
|
||||||
from pretix.plugins.banktransfer.payment import BankTransfer
|
from pretix.plugins.banktransfer.payment import BankTransfer
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
@@ -136,6 +139,11 @@ class EventWizardBasicsForm(I18nModelForm):
|
|||||||
choices=settings.LANGUAGES,
|
choices=settings.LANGUAGES,
|
||||||
label=_("Default language"),
|
label=_("Default language"),
|
||||||
)
|
)
|
||||||
|
no_taxes = forms.BooleanField(
|
||||||
|
label=_("I don't want to specify taxes now"),
|
||||||
|
help_text=_("You can always configure tax rates later."),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
tax_rate = forms.DecimalField(
|
tax_rate = forms.DecimalField(
|
||||||
label=_("Sales tax rate"),
|
label=_("Sales tax rate"),
|
||||||
help_text=_("Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate "
|
help_text=_("Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate "
|
||||||
@@ -223,6 +231,11 @@ class EventWizardBasicsForm(I18nModelForm):
|
|||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'timezone': _('Your default locale must be specified.')
|
'timezone': _('Your default locale must be specified.')
|
||||||
})
|
})
|
||||||
|
if not data.get("no_taxes") and not data.get("tax_rate"):
|
||||||
|
raise ValidationError({
|
||||||
|
'tax_rate': _('You have not specified a tax rate. If you do not want us to compute sales taxes, please '
|
||||||
|
'check "{field}" above.').format(field=self.fields["no_taxes"].label)
|
||||||
|
})
|
||||||
|
|
||||||
# change timezone
|
# change timezone
|
||||||
zone = ZoneInfo(data.get('timezone'))
|
zone = ZoneInfo(data.get('timezone'))
|
||||||
@@ -353,14 +366,9 @@ class EventUpdateForm(I18nModelForm):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.change_slug = kwargs.pop('change_slug', False)
|
self.change_slug = kwargs.pop('change_slug', False)
|
||||||
self.domain = kwargs.pop('domain', False)
|
|
||||||
|
|
||||||
kwargs.setdefault('initial', {})
|
kwargs.setdefault('initial', {})
|
||||||
self.instance = kwargs['instance']
|
self.instance = kwargs['instance']
|
||||||
if self.domain and self.instance:
|
|
||||||
initial_domain = self.instance.domains.first()
|
|
||||||
if initial_domain:
|
|
||||||
kwargs['initial'].setdefault('domain', initial_domain.domainname)
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if not self.change_slug:
|
if not self.change_slug:
|
||||||
@@ -369,48 +377,54 @@ class EventUpdateForm(I18nModelForm):
|
|||||||
self.fields['location'].widget.attrs['placeholder'] = _(
|
self.fields['location'].widget.attrs['placeholder'] = _(
|
||||||
'Sample Conference Center\nHeidelberg, Germany'
|
'Sample Conference Center\nHeidelberg, Germany'
|
||||||
)
|
)
|
||||||
if self.domain:
|
|
||||||
|
try:
|
||||||
self.fields['domain'] = forms.CharField(
|
self.fields['domain'] = forms.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
label=_('Custom domain'),
|
label=_('Domain'),
|
||||||
|
initial=self.instance.domain.domainname,
|
||||||
|
required=False,
|
||||||
|
disabled=True,
|
||||||
|
help_text=_('You can configure this in your organizer settings.')
|
||||||
|
)
|
||||||
|
except KnownDomain.DoesNotExist:
|
||||||
|
domain = get_organizer_domain(self.instance.organizer)
|
||||||
|
try:
|
||||||
|
current_domain_assignment = self.instance.alternative_domain_assignment
|
||||||
|
except AlternativeDomainAssignment.DoesNotExist:
|
||||||
|
current_domain_assignment = None
|
||||||
|
self.fields['domain'] = forms.ChoiceField(
|
||||||
|
label=_('Domain'),
|
||||||
|
help_text=_('You can add more domains in your organizer account.'),
|
||||||
|
choices=[('', _('Same as organizer account') + (f" ({domain})" if domain else ""))] + [
|
||||||
|
(d.domainname, d.domainname) for d in self.instance.organizer.domains.filter(mode=KnownDomain.MODE_ORG_ALT_DOMAIN)
|
||||||
|
],
|
||||||
|
initial=current_domain_assignment.domain_id if current_domain_assignment else "",
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('You need to configure the custom domain in the webserver beforehand.')
|
|
||||||
)
|
)
|
||||||
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
||||||
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
|
self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
|
||||||
'data-inverse-dependency': '<[name$=all_sales_channels]',
|
'data-inverse-dependency': '<[name$=all_sales_channels]',
|
||||||
}, choices=self.fields['limit_sales_channels'].widget.choices)
|
}, choices=self.fields['limit_sales_channels'].widget.choices)
|
||||||
|
|
||||||
def clean_domain(self):
|
|
||||||
d = self.cleaned_data['domain']
|
|
||||||
if d:
|
|
||||||
if d == urlparse(settings.SITE_URL).hostname:
|
|
||||||
raise ValidationError(
|
|
||||||
_('You cannot choose the base domain of this installation.')
|
|
||||||
)
|
|
||||||
if KnownDomain.objects.filter(domainname=d).exclude(event=self.instance.pk).exists():
|
|
||||||
raise ValidationError(
|
|
||||||
_('This domain is already in use for a different event or organizer.')
|
|
||||||
)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
instance = super().save(commit)
|
instance = super().save(commit)
|
||||||
|
|
||||||
if self.domain:
|
try:
|
||||||
current_domain = instance.domains.first()
|
current_domain_assignment = instance.alternative_domain_assignment
|
||||||
if self.cleaned_data['domain']:
|
except AlternativeDomainAssignment.DoesNotExist:
|
||||||
if current_domain and current_domain.domainname != self.cleaned_data['domain']:
|
current_domain_assignment = None
|
||||||
current_domain.delete()
|
if self.cleaned_data['domain'] and not hasattr(instance, 'domain'):
|
||||||
KnownDomain.objects.create(
|
domain = self.instance.organizer.domains.get(mode=KnownDomain.MODE_ORG_ALT_DOMAIN, domainname=self.cleaned_data["domain"])
|
||||||
organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain']
|
AlternativeDomainAssignment.objects.update_or_create(
|
||||||
)
|
event=instance,
|
||||||
elif not current_domain:
|
defaults={
|
||||||
KnownDomain.objects.create(
|
"domain": domain,
|
||||||
organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain']
|
}
|
||||||
)
|
)
|
||||||
elif current_domain:
|
instance.cache.clear()
|
||||||
current_domain.delete()
|
elif current_domain_assignment:
|
||||||
|
current_domain_assignment.delete()
|
||||||
instance.cache.clear()
|
instance.cache.clear()
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
@@ -1372,7 +1386,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
|||||||
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
|
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
|
||||||
|
|
||||||
for k, v in self.base_context.items():
|
for k, v in self.base_context.items():
|
||||||
self._set_field_placeholders(k, v)
|
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_'))
|
||||||
|
|
||||||
for k, v in list(self.fields.items()):
|
for k, v in list(self.fields.items()):
|
||||||
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
|
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
|
||||||
@@ -1491,6 +1505,11 @@ class TaxRuleLineForm(I18nForm):
|
|||||||
('require_approval', _('Order requires approval')),
|
('require_approval', _('Order requires approval')),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
code = forms.ChoiceField(
|
||||||
|
label=_("Tax code"),
|
||||||
|
choices=[("", _("Default tax code")), *TAX_CODE_LISTS],
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
rate = forms.DecimalField(
|
rate = forms.DecimalField(
|
||||||
label=_('Deviating tax rate'),
|
label=_('Deviating tax rate'),
|
||||||
max_digits=10, decimal_places=2,
|
max_digits=10, decimal_places=2,
|
||||||
@@ -1505,6 +1524,43 @@ class TaxRuleLineForm(I18nForm):
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.parent_form = kwargs.pop("parent_form")
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
d = super().clean()
|
||||||
|
|
||||||
|
parent_code = self.parent_form.cleaned_data.get("code")
|
||||||
|
parent_rate = self.parent_form.cleaned_data.get("rate")
|
||||||
|
|
||||||
|
code = d.get("code") or parent_code
|
||||||
|
rate = d.get("rate")
|
||||||
|
if rate is None:
|
||||||
|
rate = parent_rate
|
||||||
|
|
||||||
|
if d.get("action") in ("reverse", "no", "block") and d.get("rate"):
|
||||||
|
raise ValidationError(_("A combination of this calculation mode with a non-zero tax rate does not make sense."))
|
||||||
|
|
||||||
|
if d.get("action") == "reverse" and d.get("code") and code != "AE":
|
||||||
|
# Reverse charge but code is not reverse charge -- this is the one case we ignore if the "default code"
|
||||||
|
# is used because it is the one scenario we can auto-fix
|
||||||
|
raise ValidationError(_("This combination of calculation mode and tax code does not make sense."))
|
||||||
|
|
||||||
|
if d.get("action") == "no" and code and code.split("/")[0] in ("S", "AE", "L", "M", "B"):
|
||||||
|
# No VAT but code indicates VAT
|
||||||
|
raise ValidationError(_("This combination of calculation mode and tax code does not make sense."))
|
||||||
|
|
||||||
|
if d.get("action") == "vat" and code and rate != Decimal("0.00") and code.split("/")[0] in ("O", "E", "Z", "G", "K", "AE"):
|
||||||
|
# VAT, but code indicates exempt
|
||||||
|
raise ValidationError(_("A combination of this tax code with a non-zero tax rate does not make sense."))
|
||||||
|
|
||||||
|
if d.get("action") == "vat" and code and rate == Decimal("0.00") and code.split("/")[0] in ("S", "L", "M", "B"):
|
||||||
|
# no VAT, but code indicates non-exempt
|
||||||
|
raise ValidationError(_("A combination of this tax code with a zero tax rate does not make sense."))
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
class I18nBaseFormSet(I18nFormSetMixin, forms.BaseFormSet):
|
class I18nBaseFormSet(I18nFormSetMixin, forms.BaseFormSet):
|
||||||
# compatibility shim for django-i18nfield library
|
# compatibility shim for django-i18nfield library
|
||||||
@@ -1516,8 +1572,16 @@ class I18nBaseFormSet(I18nFormSetMixin, forms.BaseFormSet):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTaxRuleLineFormSet(I18nBaseFormSet):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.parent_form = kwargs.pop('parent_form')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.form_kwargs['parent_form'] = self.parent_form
|
||||||
|
|
||||||
|
|
||||||
TaxRuleLineFormSet = formset_factory(
|
TaxRuleLineFormSet = formset_factory(
|
||||||
TaxRuleLineForm, formset=I18nBaseFormSet,
|
TaxRuleLineForm, formset=BaseTaxRuleLineFormSet,
|
||||||
can_order=True, can_delete=True, extra=0
|
can_order=True, can_delete=True, extra=0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1525,7 +1589,16 @@ TaxRuleLineFormSet = formset_factory(
|
|||||||
class TaxRuleForm(I18nModelForm):
|
class TaxRuleForm(I18nModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TaxRule
|
model = TaxRule
|
||||||
fields = ['name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name', 'keep_gross_if_rate_changes']
|
fields = [
|
||||||
|
'name',
|
||||||
|
'rate',
|
||||||
|
'price_includes_tax',
|
||||||
|
'code',
|
||||||
|
'eu_reverse_charge',
|
||||||
|
'home_country',
|
||||||
|
'internal_name',
|
||||||
|
'keep_gross_if_rate_changes'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class WidgetCodeForm(forms.Form):
|
class WidgetCodeForm(forms.Form):
|
||||||
|
|||||||
@@ -549,7 +549,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
)
|
)
|
||||||
email = forms.CharField(
|
email = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('E-mail address')
|
label=_('Email address')
|
||||||
)
|
)
|
||||||
comment = forms.CharField(
|
comment = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@@ -563,7 +563,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
email_known_to_work = forms.NullBooleanField(
|
email_known_to_work = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
widget=FilterNullBooleanSelect,
|
widget=FilterNullBooleanSelect,
|
||||||
label=_('E-mail address verified'),
|
label=_('Email address verified'),
|
||||||
)
|
)
|
||||||
total = forms.DecimalField(
|
total = forms.DecimalField(
|
||||||
localize=True,
|
localize=True,
|
||||||
@@ -648,7 +648,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
|||||||
)
|
)
|
||||||
self.fields['attendee_email'] = forms.CharField(
|
self.fields['attendee_email'] = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Attendee e-mail address')
|
label=_('Attendee email address')
|
||||||
)
|
)
|
||||||
self.fields['attendee_address_company'] = forms.CharField(
|
self.fields['attendee_address_company'] = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@@ -1967,7 +1967,7 @@ class CheckinListAttendeeFilterForm(FilterForm):
|
|||||||
if s == '1':
|
if s == '1':
|
||||||
qs = qs.filter(last_entry__isnull=False)
|
qs = qs.filter(last_entry__isnull=False)
|
||||||
elif s == '2':
|
elif s == '2':
|
||||||
qs = qs.filter(pk__in=self.list.positions_inside.values_list('pk'))
|
qs = self.list._filter_positions_inside(qs)
|
||||||
elif s == '3':
|
elif s == '3':
|
||||||
qs = qs.filter(last_entry__isnull=False).filter(
|
qs = qs.filter(last_entry__isnull=False).filter(
|
||||||
Q(last_exit__isnull=False) & Q(last_exit__gte=F('last_entry'))
|
Q(last_exit__isnull=False) & Q(last_exit__gte=F('last_entry'))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user