forked from CGM_Public/pretix_original
Compare commits
1 Commits
metrics_mi
...
subevent-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df656d1580 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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 -y gettext unzip
|
run: sudo apt update && sudo apt install 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
|
||||||
|
|||||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -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 -y enchant-2 hunspell aspell-en
|
run: sudo apt update && sudo apt install 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
|
||||||
|
|||||||
6
.github/workflows/strings.yml
vendored
6
.github/workflows/strings.yml
vendored
@@ -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 -y install gettext
|
run: sudo apt update && sudo apt install gettext
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install uv && uv pip install --system -e ".[dev]"
|
run: pip3 install -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 uv && uv pip install --system -e ".[dev]"
|
run: pip3 install -e ".[dev]"
|
||||||
- name: Spellcheck translations
|
- name: Spellcheck translations
|
||||||
run: potypo
|
run: potypo
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
|
|||||||
4
.github/workflows/style.yml
vendored
4
.github/workflows/style.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
run: pip3 install -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 uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
run: pip3 install -e ".[dev]" psycopg2-binary
|
||||||
- name: Run flake8
|
- name: Run flake8
|
||||||
run: flake8 .
|
run: flake8 .
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
|
|||||||
32
.github/workflows/tests.yml
vendored
32
.github/workflows/tests.yml
vendored
@@ -30,21 +30,15 @@ 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:
|
||||||
@@ -56,9 +50,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 -y gettext
|
run: sudo apt update && sudo apt install gettext
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
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
|
||||||
- name: Run checks
|
- name: Run checks
|
||||||
run: python manage.py check
|
run: python manage.py check
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
@@ -70,15 +64,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/ci_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml tests --maxfail=100
|
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
|
||||||
- name: Run concurrency tests
|
- name: Run concurrency tests
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reuse-db
|
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
|
||||||
if: matrix.database == 'postgres'
|
if: matrix.database == 'postgres'
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v1
|
||||||
with:
|
with:
|
||||||
file: src/coverage.xml
|
file: src/coverage.xml
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: true
|
||||||
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
||||||
|
|||||||
@@ -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/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
|
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
|
||||||
except:
|
except:
|
||||||
- pypi
|
- pypi
|
||||||
pypi:
|
pypi:
|
||||||
|
|||||||
@@ -60,14 +60,6 @@ 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,26 +288,17 @@ 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
|
||||||
setting is not provided, pretix will generate a random secret on the first start
|
setting is not provided, pretix will generate a random secret on the first start
|
||||||
and will store it in the filesystem for later usage.
|
and will store it in the filesystem for later usage.
|
||||||
|
|
||||||
``secret_fallback0`` ... ``secret_fallback9``
|
|
||||||
Prior versions of the secret to be used by Django for signing and verification purposes that will still
|
|
||||||
be accepted but no longer be used for new signing.
|
|
||||||
|
|
||||||
``debug``
|
``debug``
|
||||||
Whether or not to run in debug mode. Default is ``False``.
|
Whether or not to run in debug mode. Default is ``False``.
|
||||||
|
|
||||||
.. 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,10 +231,11 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
listen 443 ssl default_server;
|
listen 443 default_server;
|
||||||
listen [::]:443 ipv6only=on ssl default_server;
|
listen [::]:443 ipv6only=on 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,10 +216,11 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
listen 443 ssl default_server;
|
listen 443 default_server;
|
||||||
listen [::]:443 ipv6only=on ssl default_server;
|
listen [::]:443 ipv6only=on 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;
|
||||||
|
|
||||||
@@ -248,14 +249,6 @@ 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,
|
||||||
|
|||||||
@@ -23,22 +23,6 @@ position integer An integer, use
|
|||||||
is_addon boolean If ``true``, items within this category are not on sale
|
is_addon boolean If ``true``, items within this category are not on sale
|
||||||
on their own but the category provides a source for
|
on their own but the category provides a source for
|
||||||
defining add-ons for other products.
|
defining add-ons for other products.
|
||||||
cross_selling_mode string If ``null``, cross-selling is disabled for this category.
|
|
||||||
If ``"only"``, it is only visible in the cross-selling
|
|
||||||
step.
|
|
||||||
If ``"both"``, it is visible on the normal index page
|
|
||||||
as well.
|
|
||||||
Only available if ``is_addon`` is ``false``.
|
|
||||||
cross_selling_condition string Only relevant if ``cross_selling_mode`` is not ``null``.
|
|
||||||
If ``"always"``, always show in cross-selling step.
|
|
||||||
If ``"products"``, only show if the cart contains one of
|
|
||||||
the products listed in ``cross_selling_match_products``.
|
|
||||||
If ``"discounts"``, only show products that qualify for
|
|
||||||
a discount according to discount rules.
|
|
||||||
cross_selling_match_products list of integer Only relevant if ``cross_selling_condition`` is
|
|
||||||
``"products"``. Internal ID of the items of which at
|
|
||||||
least one needs to be in the cart for this category to
|
|
||||||
be shown.
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -76,10 +60,7 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false,
|
"is_addon": false
|
||||||
"cross_selling_mode": null,
|
|
||||||
"cross_selling_condition": null,
|
|
||||||
"cross_selling_match_products": []
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -121,10 +102,7 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false,
|
"is_addon": false
|
||||||
"cross_selling_mode": null,
|
|
||||||
"cross_selling_condition": null,
|
|
||||||
"cross_selling_match_products": []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -152,10 +130,7 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false,
|
"is_addon": false
|
||||||
"cross_selling_mode": null,
|
|
||||||
"cross_selling_condition": null,
|
|
||||||
"cross_selling_match_products": []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -172,10 +147,7 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": false,
|
"is_addon": false
|
||||||
"cross_selling_mode": null,
|
|
||||||
"cross_selling_condition": null,
|
|
||||||
"cross_selling_match_products": []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event to create a category for
|
:param organizer: The ``slug`` field of the organizer of the event to create a category for
|
||||||
@@ -221,10 +193,7 @@ Endpoints
|
|||||||
"internal_name": "",
|
"internal_name": "",
|
||||||
"description": {"en": "Tickets are what you need to get in."},
|
"description": {"en": "Tickets are what you need to get in."},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"is_addon": true,
|
"is_addon": true
|
||||||
"cross_selling_mode": null,
|
|
||||||
"cross_selling_condition": null,
|
|
||||||
"cross_selling_match_products": []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ 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.
|
||||||
@@ -89,7 +91,10 @@ 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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -141,7 +146,10 @@ 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
|
||||||
@@ -238,7 +246,10 @@ 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**:
|
||||||
@@ -260,7 +271,10 @@ 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
|
||||||
@@ -312,7 +326,10 @@ 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
|
||||||
@@ -325,7 +342,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,7 +97,6 @@ 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
|
||||||
@@ -127,10 +126,6 @@ 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
|
||||||
--------------------
|
--------------------
|
||||||
@@ -208,7 +203,6 @@ 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"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -348,7 +342,6 @@ 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"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -359,12 +352,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 number: The ``number`` field of the invoice to fetch
|
:param invoice_no: The ``invoice_no`` 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/(number)/download/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/download/
|
||||||
|
|
||||||
Download an invoice in PDF format.
|
Download an invoice in PDF format.
|
||||||
|
|
||||||
@@ -391,7 +384,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 number: The ``number`` field of the invoice to fetch
|
:param invoice_no: The ``invoice_no`` 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.
|
||||||
@@ -404,7 +397,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/(number)/reissue/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/reissue/
|
||||||
|
|
||||||
Cancels the invoice and creates a new one.
|
Cancels the invoice and creates a new one.
|
||||||
|
|
||||||
@@ -426,13 +419,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 number: The ``number`` field of the invoice to reissue
|
:param invoice_no: The ``invoice_no`` 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/(number)/regenerate/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/regenerate/
|
||||||
|
|
||||||
Re-generates the invoice from order data.
|
Re-generates the invoice from order data.
|
||||||
|
|
||||||
@@ -454,7 +447,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 number: The ``number`` field of the invoice to regenerate
|
:param invoice_no: The ``invoice_no`` 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
|
||||||
|
|||||||
@@ -69,10 +69,6 @@ hidden_if_available integer **DEPRECATED*
|
|||||||
hidden_if_item_available integer The internal ID of a different item, or ``null``. If
|
hidden_if_item_available integer The internal ID of a different item, or ``null``. If
|
||||||
set, this item won't be shown publicly as long as this
|
set, this item won't be shown publicly as long as this
|
||||||
other item is available.
|
other item is available.
|
||||||
hidden_if_item_available_mode string If ``hide`` (the default), this item is hidden in the shop
|
|
||||||
if unavailable due to the ``hidden_if_item_available`` setting.
|
|
||||||
If ``info``, the item is visible, but can't be purchased,
|
|
||||||
and a note explaining the unavailability is displayed.
|
|
||||||
require_voucher boolean If ``true``, this item can only be bought using a
|
require_voucher boolean If ``true``, this item can only be bought using a
|
||||||
voucher that is specifically assigned to this item.
|
voucher that is specifically assigned to this item.
|
||||||
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
|
hide_without_voucher boolean If ``true``, this item is only shown during the voucher
|
||||||
@@ -243,10 +239,6 @@ meta_data object Values set fo
|
|||||||
The ``hidden_if_item_available`` attributes has been added, the ``hidden_if_available`` attribute has been
|
The ``hidden_if_item_available`` attributes has been added, the ``hidden_if_available`` attribute has been
|
||||||
deprecated.
|
deprecated.
|
||||||
|
|
||||||
.. versionchanged:: 2025.01
|
|
||||||
|
|
||||||
The ``hidden_if_item_available_mode`` attributes has been added.
|
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
|
|
||||||
@@ -316,7 +308,6 @@ Endpoints
|
|||||||
"available_until_mode": "hide",
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"hidden_if_item_available_mode": "hide",
|
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
@@ -468,7 +459,6 @@ Endpoints
|
|||||||
"available_until_mode": "hide",
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"hidden_if_item_available_mode": "hide",
|
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
@@ -599,7 +589,6 @@ Endpoints
|
|||||||
"available_until_mode": "hide",
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"hidden_if_item_available_mode": "hide",
|
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
@@ -716,7 +705,6 @@ Endpoints
|
|||||||
"available_until_mode": "hide",
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"hidden_if_item_available_mode": "hide",
|
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"allow_cancel": true,
|
"allow_cancel": true,
|
||||||
@@ -867,7 +855,6 @@ Endpoints
|
|||||||
"available_until_mode": "hide",
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"hidden_if_item_available_mode": "hide",
|
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"generate_tickets": null,
|
"generate_tickets": null,
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ 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
|
||||||
@@ -105,10 +104,6 @@ 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.
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -156,13 +151,6 @@ cancellation_date datetime Time of order c
|
|||||||
|
|
||||||
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:
|
||||||
|
|
||||||
@@ -200,7 +188,6 @@ 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``)
|
||||||
@@ -219,17 +206,6 @@ 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
|
||||||
@@ -257,14 +233,6 @@ 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
|
||||||
@@ -416,7 +384,6 @@ 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,
|
||||||
@@ -432,21 +399,10 @@ 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,
|
||||||
@@ -482,15 +438,14 @@ 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``, ``status`` and ``cancellation_date``. Default: ``datetime``
|
``last_modified``, and ``status``. 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)
|
||||||
@@ -656,7 +611,6 @@ 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,
|
||||||
@@ -672,22 +626,10 @@ 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,
|
||||||
@@ -723,8 +665,7 @@ 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
|
||||||
@@ -855,7 +796,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`` and ``web_secret`` attributes for both the order and all order positions.
|
Triggers generation of new ``secret`` attributes for both the order and all order positions.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@@ -886,7 +827,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`` and ``web_secret`` attribute for a single order position.
|
Triggers generation of a new ``secret`` attribute for a single order position.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@@ -1036,8 +977,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``
|
||||||
|
|
||||||
@@ -1625,7 +1566,6 @@ 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",
|
||||||
@@ -1641,22 +1581,10 @@ 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,
|
||||||
@@ -1752,7 +1680,6 @@ 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,
|
||||||
@@ -1768,22 +1695,10 @@ 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,
|
||||||
@@ -1880,10 +1795,6 @@ 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:
|
||||||
@@ -2143,59 +2054,6 @@ 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
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Endpoints
|
|||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"identifier": "web",
|
"identifier": "web",
|
||||||
"label": {
|
"name": {
|
||||||
"en": "Online shop"
|
"en": "Online shop"
|
||||||
},
|
},
|
||||||
"type": "web",
|
"type": "web",
|
||||||
@@ -88,7 +88,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "web",
|
"identifier": "web",
|
||||||
"label": {
|
"name": {
|
||||||
"en": "Online shop"
|
"en": "Online shop"
|
||||||
},
|
},
|
||||||
"type": "web",
|
"type": "web",
|
||||||
@@ -116,7 +116,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "api.custom",
|
"identifier": "api.custom",
|
||||||
"label": {
|
"name": {
|
||||||
"en": "Custom integration"
|
"en": "Custom integration"
|
||||||
},
|
},
|
||||||
"type": "api",
|
"type": "api",
|
||||||
@@ -133,7 +133,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "api.custom",
|
"identifier": "api.custom",
|
||||||
"label": {
|
"name": {
|
||||||
"en": "Custom integration"
|
"en": "Custom integration"
|
||||||
},
|
},
|
||||||
"type": "api",
|
"type": "api",
|
||||||
@@ -178,7 +178,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "web",
|
"identifier": "web",
|
||||||
"label": {
|
"name": {
|
||||||
"en": "Online shop"
|
"en": "Online shop"
|
||||||
},
|
},
|
||||||
"type": "web",
|
"type": "web",
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.. _`rest-seats`:
|
.. _`rest-reusablemedia`:
|
||||||
|
|
||||||
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,114 +260,3 @@ 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.
|
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query page: The page number in case of a multi-page result set, default is 1
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
:query is_public: If set to ``true``/``false``, only subevents with a matching value of ``is_public`` are returned.
|
|
||||||
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
||||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
||||||
@@ -468,7 +467,6 @@ Endpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
:query page: The page number in case of a multi-page result set, default is 1
|
:query page: The page number in case of a multi-page result set, default is 1
|
||||||
:query is_public: If set to ``true``/``false``, only subevents with a matching value of ``is_public`` are returned.
|
|
||||||
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
:query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned.
|
||||||
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
|
:query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned.
|
||||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
.. spelling:word-list::
|
|
||||||
|
|
||||||
EN16931
|
|
||||||
DSFinV-K
|
|
||||||
|
|
||||||
.. _rest-taxrules:
|
.. _rest-taxrules:
|
||||||
|
|
||||||
Tax rules
|
Tax rules
|
||||||
@@ -23,7 +18,6 @@ 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
|
||||||
@@ -48,42 +42,6 @@ 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
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -116,7 +74,6 @@ 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,
|
||||||
@@ -158,7 +115,6 @@ 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,
|
||||||
@@ -208,7 +164,6 @@ 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,
|
||||||
@@ -257,7 +212,6 @@ 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,
|
||||||
@@ -304,4 +258,3 @@ 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,7 +17,6 @@ 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:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Core
|
|||||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
||||||
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
||||||
register_ticket_secret_generators, gift_card_transaction_display,
|
register_ticket_secret_generators, gift_card_transaction_display,
|
||||||
register_text_placeholders, register_mail_placeholders, device_info_updated
|
register_text_placeholders, register_mail_placeholders
|
||||||
|
|
||||||
Order events
|
Order events
|
||||||
""""""""""""
|
""""""""""""
|
||||||
@@ -22,14 +22,12 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -41,21 +39,18 @@ 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
|
||||||
@@ -67,28 +62,24 @@ 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
|
||||||
@@ -98,9 +89,4 @@ 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,7 +60,6 @@ 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,6 +84,8 @@ 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,10 +86,7 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ This will automatically make pretix discover this plugin as soon as it is instal
|
|||||||
through ``pip``. During development, you can just run ``python setup.py develop`` inside
|
through ``pip``. During development, you can just run ``python setup.py develop`` inside
|
||||||
your plugin source directory to make it discoverable.
|
your plugin source directory to make it discoverable.
|
||||||
|
|
||||||
.. _`signals`:
|
|
||||||
Signals
|
Signals
|
||||||
-------
|
-------
|
||||||
|
|
||||||
@@ -154,25 +153,6 @@ in the ``installed`` method:
|
|||||||
Note that ``installed`` will *not* be called if the plugin is indirectly activated for an event
|
Note that ``installed`` will *not* be called if the plugin is indirectly activated for an event
|
||||||
because the event is created with settings copied from another event.
|
because the event is created with settings copied from another event.
|
||||||
|
|
||||||
.. _`registries`:
|
|
||||||
Registries
|
|
||||||
----------
|
|
||||||
|
|
||||||
Many signals in pretix are used so that plugins can "register" a class, e.g. a payment provider or a
|
|
||||||
ticket renderer.
|
|
||||||
|
|
||||||
However, for some of them (types of :ref:`Log Entries <logging>`) we use a different method to keep track of them:
|
|
||||||
In a ``Registry``, classes are collected at application startup, along with a unique key (in case
|
|
||||||
of LogEntryType, the ``action_type``) as well as which plugin registered them.
|
|
||||||
|
|
||||||
To register a class, you can use one of several decorators provided by the Registry object:
|
|
||||||
|
|
||||||
.. autoclass:: pretix.base.logentrytypes.LogEntryTypeRegistry
|
|
||||||
:members: register, new, new_from_dict
|
|
||||||
|
|
||||||
All files in which classes are registered need to be imported in the ``AppConfig.ready`` as explained
|
|
||||||
in `Signals <signals>`_ above.
|
|
||||||
|
|
||||||
Views
|
Views
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ To actually log an action, you can just call the ``log_action`` method on your o
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
order.log_action('pretix.event.order.comment', user=user,
|
order.log_action('pretix.event.order.canceled', user=user, data={})
|
||||||
data={"new_comment": "Hello, world."})
|
|
||||||
|
|
||||||
The positional ``action`` argument should represent the type of action and should be globally unique, we
|
The positional ``action`` argument should represent the type of action and should be globally unique, we
|
||||||
recommend to prefix it with your package name, e.g. ``paypal.payment.rejected``. The ``user`` argument is
|
recommend to prefix it with your package name, e.g. ``paypal.payment.rejected``. The ``user`` argument is
|
||||||
@@ -73,101 +72,24 @@ following ready-to-include template::
|
|||||||
{% include "pretixcontrol/includes/logs.html" with obj=order %}
|
{% include "pretixcontrol/includes/logs.html" with obj=order %}
|
||||||
|
|
||||||
We now need a way to translate the action codes like ``pretix.event.changed`` into human-readable
|
We now need a way to translate the action codes like ``pretix.event.changed`` into human-readable
|
||||||
strings. The :py:attr:`pretix.base.logentrytypes.log_entry_types` :ref:`registry <registries>` allows you to do so. A simple
|
strings. The :py:attr:`pretix.base.signals.logentry_display` signals allows you to do so. A simple
|
||||||
implementation could look like:
|
implementation could look like:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from pretix.base.logentrytypes import log_entry_types
|
from pretix.base.signals import logentry_display
|
||||||
|
|
||||||
|
|
||||||
@log_entry_types.new_from_dict({
|
|
||||||
'pretix.event.order.comment': _('The order\'s internal comment has been updated to: {new_comment}'),
|
|
||||||
'pretix.event.order.paid': _('The order has been marked as paid.'),
|
|
||||||
# ...
|
|
||||||
})
|
|
||||||
class CoreOrderLogEntryType(OrderLogEntryType):
|
|
||||||
pass
|
|
||||||
|
|
||||||
Please note that you always need to define your own inherited ``LogEntryType`` class in your plugin. If you would just
|
|
||||||
register an instance of a ``LogEntryType`` class defined in pretix core, it cannot be automatically detected as belonging
|
|
||||||
to your plugin, leading to confusing user interface situations.
|
|
||||||
|
|
||||||
|
|
||||||
Customizing log entry display
|
|
||||||
"""""""""""""""""""""""""""""
|
|
||||||
|
|
||||||
The base ``LogEntryType`` classes allow for varying degree of customization in their descendants.
|
|
||||||
|
|
||||||
If you want to add another log message for an existing core object (e.g. an :class:`Order <pretix.base.models.Order>`,
|
|
||||||
:class:`Item <pretix.base.models.Item>`, or :class:`Voucher <pretix.base.models.Voucher>`), you can inherit
|
|
||||||
from its predefined :class:`LogEntryType <pretix.base.logentrytypes.LogEntryType>`, e.g.
|
|
||||||
:class:`OrderLogEntryType <pretix.base.logentrytypes.OrderLogEntryType>`, and just specify a new plaintext string.
|
|
||||||
You can use format strings to insert information from the LogEntry's `data` object as shown in the section above.
|
|
||||||
|
|
||||||
If you define a new model object in your plugin, you should make sure proper object links in the user interface are
|
|
||||||
displayed for it. If your model object belongs logically to a pretix :class:`Event <pretix.base.models.Event>`, you can inherit from :class:`EventLogEntryType <pretix.base.logentrytypes.EventLogEntryType>`,
|
|
||||||
and set the ``object_link_*`` fields accordingly. ``object_link_viewname`` refers to a django url name, which needs to
|
|
||||||
accept the arguments `organizer` and `event`, containing the respective slugs, and additional arguments provided by
|
|
||||||
``object_link_args``. The default implementation of ``object_link_args`` will return an argument named by
|
|
||||||
````object_link_argname``, with a value of ``content_object.pk`` (the primary key of the model object).
|
|
||||||
If you want to customize the name displayed for the object (instead of the result of calling ``str()`` on it),
|
|
||||||
overwrite ``object_link_display_name``.
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class ItemLogEntryType(EventLogEntryType):
|
|
||||||
object_link_wrapper = _('Product {val}')
|
|
||||||
|
|
||||||
# link will be generated as reverse('control:event.item', {'organizer': ..., 'event': ..., 'item': item.pk})
|
|
||||||
object_link_viewname = 'control:event.item'
|
|
||||||
object_link_argname = 'item'
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class OrderLogEntryType(EventLogEntryType):
|
|
||||||
object_link_wrapper = _('Order {val}')
|
|
||||||
|
|
||||||
# link will be generated as reverse('control:event.order', {'organizer': ..., 'event': ..., 'code': order.code})
|
|
||||||
object_link_viewname = 'control:event.order'
|
|
||||||
|
|
||||||
def object_link_args(self, order):
|
|
||||||
return {'code': order.code}
|
|
||||||
|
|
||||||
def object_link_display_name(self, order):
|
|
||||||
return order.code
|
|
||||||
|
|
||||||
To show more sophisticated message strings, e.g. varying the message depending on information from the :class:`LogEntry <pretix.base.models.log.LogEntry>`'s
|
|
||||||
`data` object, override the `display` method:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@log_entry_types.new()
|
|
||||||
class PaypalEventLogEntryType(EventLogEntryType):
|
|
||||||
action_type = 'pretix.plugins.paypal.event'
|
|
||||||
|
|
||||||
def display(self, logentry):
|
|
||||||
event_type = logentry.parsed_data.get('event_type')
|
|
||||||
text = {
|
|
||||||
'PAYMENT.SALE.COMPLETED': _('Payment completed.'),
|
|
||||||
'PAYMENT.SALE.DENIED': _('Payment denied.'),
|
|
||||||
# ...
|
|
||||||
}.get(event_type, f"({event_type})")
|
|
||||||
return _('PayPal reported an event: {}').format(text)
|
|
||||||
|
|
||||||
.. automethod:: pretix.base.logentrytypes.LogEntryType.display
|
|
||||||
|
|
||||||
If your new model object does not belong to an :class:`Event <pretix.base.models.Event>`, you need to inherit directly from ``LogEntryType`` instead
|
|
||||||
of ``EventLogEntryType``, providing your own implementation of ``get_object_link_info`` if object links should be
|
|
||||||
displayed.
|
|
||||||
|
|
||||||
.. autoclass:: pretix.base.logentrytypes.LogEntryType
|
|
||||||
:members: get_object_link_info
|
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signal=logentry_display)
|
||||||
|
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||||
|
plains = {
|
||||||
|
'pretix.event.order.paid': _('The order has been marked as paid.'),
|
||||||
|
'pretix.event.order.refunded': _('The order has been refunded.'),
|
||||||
|
'pretix.event.order.canceled': _('The order has been canceled.'),
|
||||||
|
...
|
||||||
|
}
|
||||||
|
if logentry.action_type in plains:
|
||||||
|
return plains[logentry.action_type]
|
||||||
|
|
||||||
Sending notifications
|
Sending notifications
|
||||||
---------------------
|
---------------------
|
||||||
|
|||||||
@@ -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`` value. This is often used in systems, where the same
|
but different ``name``s. 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
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ item_assignments list of objects Products this l
|
|||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
Layout endpoints
|
Endpoints
|
||||||
----------------
|
---------
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
||||||
|
|
||||||
@@ -268,75 +268,5 @@ Layout endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
: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.
|
||||||
|
|
||||||
Ticket rendering endpoint
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/ticketpdfrenderer/render_batch/
|
|
||||||
|
|
||||||
With this API call, you can instruct the system to render a set of tickets into one combined PDF file. To specify
|
|
||||||
which tickets to render, you need to submit a list of "parts". For every part, the following fields are supported:
|
|
||||||
|
|
||||||
* ``orderposition`` (``integer``, required): The ID of the order position to render.
|
|
||||||
* ``override_channel`` (``string``, optional): The sales channel ID to be used for layout selection instead of the
|
|
||||||
original channel of the order.
|
|
||||||
* ``override_layout`` (``integer``, optional): The ticket layout ID to be used instead of the auto-selected one.
|
|
||||||
|
|
||||||
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
|
||||||
The body points you to the download URL of the result. Running a ``GET`` request on that result URL will
|
|
||||||
yield one of the following status codes:
|
|
||||||
|
|
||||||
* ``200 OK`` – The export succeeded. The body will be your resulting file. Might be large!
|
|
||||||
* ``409 Conflict`` – Your export is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
|
||||||
* ``410 Gone`` – Running the export has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
|
||||||
* ``404 Not Found`` – The export does not exist / is expired.
|
|
||||||
|
|
||||||
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
|
|
||||||
|
|
||||||
.. note:: To avoid performance issues, a maximum number of 1000 parts is currently allowed.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/render_batch/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"parts": [
|
|
||||||
{
|
|
||||||
"orderposition": 55412
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"orderposition": 55412,
|
|
||||||
"override_channel": "web"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"orderposition": 55412,
|
|
||||||
"override_layout": 56
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/ticketpdfrenderer/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:statuscode 202: no error
|
|
||||||
:statuscode 400: Invalid input options
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|
||||||
|
|
||||||
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json
|
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/pdf-layout.schema.json
|
||||||
|
|||||||
@@ -96,18 +96,6 @@ attribute::
|
|||||||
<pretix-widget event="https://pretix.eu/demo/democon/" disable-iframe></pretix-widget>
|
<pretix-widget event="https://pretix.eu/demo/democon/" disable-iframe></pretix-widget>
|
||||||
|
|
||||||
|
|
||||||
Always show event’s info
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
If you want the widget to show the event’s info such as title, location and frontpage text, you can pass the optional
|
|
||||||
``display-event-info`` attribute with either a value of ``"false"``, ``"true"`` or ``"auto"`` – the latter being the
|
|
||||||
default if the attribute is not present at all.
|
|
||||||
|
|
||||||
Note that any other value than ``"false"`` or ``"auto"`` means ``"true"``::
|
|
||||||
|
|
||||||
<pretix-widget event="https://pretix.eu/demo/democon/" display-event-info></pretix-widget>
|
|
||||||
|
|
||||||
|
|
||||||
Pre-selecting a voucher
|
Pre-selecting a voucher
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
|||||||
@@ -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==6.2.*",
|
"bleach==5.0.*",
|
||||||
"celery==5.4.*",
|
"celery==5.4.*",
|
||||||
"chardet==5.2.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=44.0.0",
|
"cryptography>=3.4.2",
|
||||||
"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.7.*",
|
"django-hijack==3.6.*",
|
||||||
"django-i18nfield==1.10.*",
|
"django-i18nfield==1.9.*,>=1.9.4",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
"django-localflavor==4.0",
|
"django-localflavor==4.0",
|
||||||
"django-markup",
|
"django-markup",
|
||||||
@@ -53,9 +53,9 @@ 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.6.*",
|
"django-statici18n==2.5.*",
|
||||||
"djangorestframework==3.15.*",
|
"djangorestframework==3.15.*",
|
||||||
"dnspython==2.7.*",
|
"dnspython==2.6.*",
|
||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
"geoip2==4.*",
|
"geoip2==4.*",
|
||||||
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||||
@@ -72,55 +72,57 @@ dependencies = [
|
|||||||
"packaging",
|
"packaging",
|
||||||
"paypalrestsdk==1.13.*",
|
"paypalrestsdk==1.13.*",
|
||||||
"paypal-checkout-serversdk==1.0.*",
|
"paypal-checkout-serversdk==1.0.*",
|
||||||
"PyJWT==2.10.*",
|
"PyJWT==2.9.*",
|
||||||
"phonenumberslite==8.13.*",
|
"phonenumberslite==8.13.*",
|
||||||
"Pillow==11.1.*",
|
"Pillow==10.4.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==5.29.*",
|
"protobuf==5.28.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.22",
|
"pycparser==2.22",
|
||||||
"pycryptodome==3.21.*",
|
"pycryptodome==3.20.*",
|
||||||
"pypdf==5.1.*",
|
"pypdf==5.0.*",
|
||||||
"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==7.4.*",
|
||||||
"redis==5.2.*",
|
"redis==5.0.*",
|
||||||
"reportlab==4.2.*",
|
"reportlab==4.2.*",
|
||||||
"requests==2.31.*",
|
"requests==2.31.*",
|
||||||
"sentry-sdk==2.20.*",
|
"sentry-sdk==2.14.*",
|
||||||
"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==1.0.*",
|
"ua-parser==0.18.*",
|
||||||
"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.5.*",
|
"webauthn==2.2.*",
|
||||||
"zeep==4.3.*"
|
"zeep==4.2.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memcached = ["pylibmc"]
|
memcached = ["pylibmc"]
|
||||||
dev = [
|
dev = [
|
||||||
"aiohttp==3.11.*",
|
"aiohttp==3.10.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.26.*",
|
"fakeredis==2.24.*",
|
||||||
"flake8==7.1.*",
|
"flake8==7.1.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==5.13.*",
|
"isort==5.13.*",
|
||||||
"pep8-naming==0.14.*",
|
"pep8-naming==0.14.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pytest-asyncio>=0.24",
|
"pytest-asyncio",
|
||||||
"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.12.0.dev0"
|
__version__ = "2024.10.0.dev0"
|
||||||
|
|||||||
@@ -75,14 +75,6 @@ FORMAT_MODULE_PATH = [
|
|||||||
'pretix.helpers.formats',
|
'pretix.helpers.formats',
|
||||||
]
|
]
|
||||||
|
|
||||||
CORE_MODULES = {
|
|
||||||
"pretix.base",
|
|
||||||
"pretix.presale",
|
|
||||||
"pretix.control",
|
|
||||||
"pretix.plugins.checkinlists",
|
|
||||||
"pretix.plugins.reports",
|
|
||||||
}
|
|
||||||
|
|
||||||
ALL_LANGUAGES = [
|
ALL_LANGUAGES = [
|
||||||
('en', _('English')),
|
('en', _('English')),
|
||||||
('de', _('German')),
|
('de', _('German')),
|
||||||
@@ -102,7 +94,6 @@ ALL_LANGUAGES = [
|
|||||||
('el', _('Greek')),
|
('el', _('Greek')),
|
||||||
('id', _('Indonesian')),
|
('id', _('Indonesian')),
|
||||||
('it', _('Italian')),
|
('it', _('Italian')),
|
||||||
('ja', _('Japanese')),
|
|
||||||
('lv', _('Latvian')),
|
('lv', _('Latvian')),
|
||||||
('nb-no', _('Norwegian Bokmål')),
|
('nb-no', _('Norwegian Bokmål')),
|
||||||
('pl', _('Polish')),
|
('pl', _('Polish')),
|
||||||
@@ -163,12 +154,6 @@ EXTRA_LANG_INFO = {
|
|||||||
'name': 'Portuguese',
|
'name': 'Portuguese',
|
||||||
'name_local': 'Português',
|
'name_local': 'Português',
|
||||||
},
|
},
|
||||||
'nb-no': {
|
|
||||||
'bidi': False,
|
|
||||||
'code': 'nb-no',
|
|
||||||
'name': 'Norwegian Bokmal',
|
|
||||||
'name_local': 'norsk (bokmål)',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
|
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
|
||||||
@@ -302,7 +287,7 @@ PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF')
|
|||||||
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
||||||
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
|
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
|
||||||
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
|
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
|
||||||
".bmp", ".tif", ".tiff", ".ics",
|
".bmp", ".tif", ".tiff"
|
||||||
)
|
)
|
||||||
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
|
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
FullAccessSecurityProfile, get_all_security_profiles,
|
DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile,
|
||||||
)
|
)
|
||||||
from pretix.base.models import Device
|
from pretix.base.models import Device
|
||||||
|
|
||||||
@@ -58,8 +58,7 @@ 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):
|
||||||
profiles = get_all_security_profiles()
|
profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile)
|
||||||
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,40 +20,13 @@
|
|||||||
# <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 BaseSecurityProfile:
|
class FullAccessSecurityProfile:
|
||||||
@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)')
|
||||||
|
|
||||||
@@ -61,7 +34,7 @@ class FullAccessSecurityProfile(BaseSecurityProfile):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class AllowListSecurityProfile(BaseSecurityProfile):
|
class AllowListSecurityProfile:
|
||||||
allowlist = ()
|
allowlist = ()
|
||||||
|
|
||||||
def is_allowed(self, request):
|
def is_allowed(self, request):
|
||||||
@@ -104,7 +77,6 @@ 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'),
|
||||||
@@ -140,7 +112,6 @@ 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'),
|
||||||
@@ -176,7 +147,6 @@ 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'),
|
||||||
@@ -184,28 +154,87 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_all_security_profiles():
|
class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||||
global _ALL_PROFILES
|
identifier = 'pretixpos'
|
||||||
|
verbose_name = _('pretixPOS')
|
||||||
if _ALL_PROFILES:
|
allowlist = (
|
||||||
return _ALL_PROFILES
|
('GET', 'api-v1:version'),
|
||||||
|
('GET', 'api-v1:device.eventselection'),
|
||||||
types = OrderedDict()
|
('GET', 'api-v1:idempotency.query'),
|
||||||
for recv, ret in register_device_security_profile.send(None):
|
('GET', 'api-v1:device.info'),
|
||||||
if isinstance(ret, (list, tuple)):
|
('POST', 'api-v1:device.update'),
|
||||||
for r in ret:
|
('POST', 'api-v1:device.revoke'),
|
||||||
types[r.identifier] = r
|
('POST', 'api-v1:device.roll'),
|
||||||
else:
|
('GET', 'api-v1:event-list'),
|
||||||
types[ret.identifier] = ret
|
('GET', 'api-v1:event-detail'),
|
||||||
_ALL_PROFILES = types
|
('GET', 'api-v1:subevent-list'),
|
||||||
return types
|
('GET', 'api-v1:subevent-detail'),
|
||||||
|
('GET', 'api-v1:itemcategory-list'),
|
||||||
|
('GET', 'api-v1:item-list'),
|
||||||
@receiver(register_device_security_profile, dispatch_uid="base_register_default_security_profiles")
|
('GET', 'api-v1:question-list'),
|
||||||
def register_default_webhook_events(sender, **kwargs):
|
('GET', 'api-v1:quota-list'),
|
||||||
return (
|
('GET', 'api-v1:taxrule-list'),
|
||||||
FullAccessSecurityProfile(),
|
('GET', 'api-v1:ticketlayout-list'),
|
||||||
PretixScanSecurityProfile(),
|
('GET', 'api-v1:ticketlayoutitem-list'),
|
||||||
PretixScanNoSyncSecurityProfile(),
|
('GET', 'api-v1:badgelayout-list'),
|
||||||
PretixScanNoSyncNoSearchSecurityProfile(),
|
('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 = {
|
||||||
|
k.identifier: k() for k in (
|
||||||
|
FullAccessSecurityProfile,
|
||||||
|
PretixScanSecurityProfile,
|
||||||
|
PretixScanNoSyncSecurityProfile,
|
||||||
|
PretixScanNoSyncNoSearchSecurityProfile,
|
||||||
|
PretixPosSecurityProfile,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,32 +88,24 @@ class SalesChannelMigrationMixin:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
||||||
raise ValidationError({
|
raise ValidationError(
|
||||||
"limit_sales_channels": [
|
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||||
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
"the list of all sales channels."
|
||||||
"the list of all sales channels."
|
)
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
|
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
|
||||||
raise ValidationError({
|
raise ValidationError(
|
||||||
"limit_sales_channels": [
|
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||||
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
"the same list."
|
||||||
"the same list."
|
)
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
if set(data["sales_channels"]) == all_channels:
|
if 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:
|
||||||
data["all_sales_channels"] = False
|
data["all_sales_channels"] = False
|
||||||
data["limit_sales_channels"] = data["sales_channels"]
|
data["limit_sales_channels"] = data["sales_channels"]
|
||||||
del data["sales_channels"]
|
del data["sales_channels"]
|
||||||
|
|
||||||
if data.get("all_sales_channels"):
|
|
||||||
data["limit_sales_channels"] = []
|
|
||||||
|
|
||||||
return super().to_internal_value(data)
|
return super().to_internal_value(data)
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
|
|||||||
@@ -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', None)
|
validated_data.pop('sales_channel')
|
||||||
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,22 +26,31 @@ 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
|
from pretix.base.models import Checkin, CheckinList, SalesChannel
|
||||||
|
|
||||||
|
|
||||||
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', 'allow_multiple_entries', 'allow_entry_after_exit',
|
'include_pending', 'auto_checkin_sales_channels', '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
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
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,7 +43,6 @@ 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
|
||||||
|
|
||||||
@@ -437,8 +436,7 @@ 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)
|
||||||
all_sales_channels = validated_data.pop('all_sales_channels', None)
|
sales_channels = validated_data.pop('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})
|
||||||
|
|
||||||
@@ -451,9 +449,8 @@ 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 all_sales_channels is not None or limit_sales_channels is not None:
|
if sales_channels is not None:
|
||||||
new_event.all_sales_channels = all_sales_channels
|
new_event.sales_channels = 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:
|
||||||
@@ -681,8 +678,8 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TaxRule
|
model = TaxRule
|
||||||
fields = ('id', 'name', 'rate', 'code', 'price_includes_tax', 'eu_reverse_charge', 'home_country',
|
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name',
|
||||||
'internal_name', 'keep_gross_if_rate_changes', 'custom_rules')
|
'keep_gross_if_rate_changes', 'custom_rules')
|
||||||
|
|
||||||
|
|
||||||
class EventSettingsSerializer(SettingsSerializer):
|
class EventSettingsSerializer(SettingsSerializer):
|
||||||
@@ -775,7 +772,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'invoice_address_company_required',
|
'invoice_address_company_required',
|
||||||
'invoice_address_beneficiary',
|
'invoice_address_beneficiary',
|
||||||
'invoice_address_custom_field',
|
'invoice_address_custom_field',
|
||||||
'invoice_address_custom_field_helptext',
|
|
||||||
'invoice_name_required',
|
'invoice_name_required',
|
||||||
'invoice_address_not_asked_free',
|
'invoice_address_not_asked_free',
|
||||||
'invoice_show_payments',
|
'invoice_show_payments',
|
||||||
@@ -920,7 +916,6 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
|||||||
'invoice_address_company_required',
|
'invoice_address_company_required',
|
||||||
'invoice_address_beneficiary',
|
'invoice_address_beneficiary',
|
||||||
'invoice_address_custom_field',
|
'invoice_address_custom_field',
|
||||||
'invoice_address_custom_field_helptext',
|
|
||||||
'invoice_name_required',
|
'invoice_name_required',
|
||||||
'invoice_address_not_asked_free',
|
'invoice_address_not_asked_free',
|
||||||
'invoice_address_from_name',
|
'invoice_address_from_name',
|
||||||
@@ -992,40 +987,6 @@ 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,8 +19,57 @@
|
|||||||
# 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.rest_framework import I18nAwareModelSerializer, I18nField
|
from i18nfield.fields import I18nCharField, I18nTextField
|
||||||
|
from i18nfield.strings import LazyI18nString
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
|
from rest_framework.fields import Field
|
||||||
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class I18nField(Field):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.allow_blank = kwargs.pop('allow_blank', False)
|
||||||
|
self.trim_whitespace = kwargs.pop('trim_whitespace', True)
|
||||||
|
self.max_length = kwargs.pop('max_length', None)
|
||||||
|
self.min_length = kwargs.pop('min_length', None)
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
if hasattr(value, 'data'):
|
||||||
|
if isinstance(value.data, dict):
|
||||||
|
return value.data
|
||||||
|
elif value.data is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
settings.LANGUAGE_CODE: str(value.data)
|
||||||
|
}
|
||||||
|
elif value is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
settings.LANGUAGE_CODE: str(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if isinstance(data, str):
|
||||||
|
return LazyI18nString(data)
|
||||||
|
elif isinstance(data, dict):
|
||||||
|
if any([k not in dict(settings.LANGUAGES) for k in data.keys()]):
|
||||||
|
raise ValidationError('Invalid languages included.')
|
||||||
|
return LazyI18nString(data)
|
||||||
|
else:
|
||||||
|
raise ValidationError('Invalid data type.')
|
||||||
|
|
||||||
|
|
||||||
|
class I18nAwareModelSerializer(ModelSerializer):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
I18nAwareModelSerializer.serializer_field_mapping[I18nCharField] = I18nField
|
||||||
|
I18nAwareModelSerializer.serializer_field_mapping[I18nTextField] = I18nField
|
||||||
|
|
||||||
|
|
||||||
class I18nURLField(I18nField):
|
class I18nURLField(I18nField):
|
||||||
@@ -35,10 +84,3 @@ 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",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||||
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
||||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||||
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', 'allow_waitinglist',
|
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'allow_waitinglist',
|
||||||
'issue_giftcard', 'meta_data',
|
'issue_giftcard', 'meta_data',
|
||||||
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
|
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
|
||||||
'grant_membership_duration_like_event', 'grant_membership_duration_days',
|
'grant_membership_duration_like_event', 'grant_membership_duration_days',
|
||||||
@@ -369,7 +369,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||||
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||||
item = Item.objects.create(**validated_data)
|
item = Item.objects.create(**validated_data)
|
||||||
if limit_sales_channels and not validated_data.get('all_sales_channels'):
|
if limit_sales_channels:
|
||||||
item.limit_sales_channels.add(*limit_sales_channels)
|
item.limit_sales_channels.add(*limit_sales_channels)
|
||||||
if picture:
|
if picture:
|
||||||
item.picture.save(os.path.basename(picture.name), picture)
|
item.picture.save(os.path.basename(picture.name), picture)
|
||||||
@@ -441,22 +441,7 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemCategory
|
model = ItemCategory
|
||||||
fields = (
|
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon')
|
||||||
'id', 'name', 'internal_name', 'description', 'position',
|
|
||||||
'is_addon', 'cross_selling_mode',
|
|
||||||
'cross_selling_condition', 'cross_selling_match_products'
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
data = super().validate(data)
|
|
||||||
|
|
||||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
|
||||||
full_data.update(data)
|
|
||||||
|
|
||||||
if full_data.get('is_addon') and full_data.get('cross_selling_mode'):
|
|
||||||
raise ValidationError('is_addon and cross_selling_mode are mutually exclusive')
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
||||||
|
|||||||
@@ -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,
|
||||||
PrintLog, RevokedTicketSecret,
|
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,26 +284,6 @@ 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)
|
||||||
@@ -496,7 +476,6 @@ 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)
|
||||||
@@ -511,13 +490,12 @@ 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',
|
||||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id', 'pdf_data', 'seat',
|
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
||||||
'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', 'tax_code', 'pseudonymization_id',
|
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
|
||||||
'pdf_data', 'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use'
|
'seat', 'canceled', 'discount', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -599,9 +577,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',
|
||||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat',
|
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
|
||||||
'require_attention', 'order__status', 'order__valid_if_pending', 'order__require_approval',
|
'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
|
||||||
'valid_from', 'valid_until', 'blocked')
|
'blocked')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -643,8 +621,7 @@ 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',
|
fields = ('id', 'fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
||||||
'tax_code', 'canceled')
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentURLField(serializers.URLField):
|
class PaymentURLField(serializers.URLField):
|
||||||
@@ -755,12 +732,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', 'cancellation_date'
|
'url', 'customer', 'valid_if_pending', 'api_meta'
|
||||||
)
|
)
|
||||||
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', 'cancellation_date'
|
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -1517,7 +1494,6 @@ 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:
|
||||||
@@ -1678,7 +1654,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_code', 'tax_name', 'fee_type',
|
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name', 'fee_type',
|
||||||
'fee_internal_type', 'event_location')
|
'fee_internal_type', 'event_location')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ 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
|
||||||
@@ -298,7 +297,6 @@ 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
|
||||||
@@ -308,10 +306,6 @@ 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,17 +32,10 @@ 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,7 +62,6 @@ 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,
|
||||||
)
|
)
|
||||||
@@ -116,7 +115,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',
|
'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
|
||||||
)
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -143,9 +142,7 @@ 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,
|
||||||
@@ -368,9 +365,8 @@ 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]).select_related('device')
|
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
|
||||||
),
|
),
|
||||||
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(
|
||||||
@@ -382,7 +378,6 @@ 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',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -394,9 +389,8 @@ 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]).select_related('device')
|
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
|
||||||
),
|
),
|
||||||
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,7 +20,6 @@
|
|||||||
# <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
|
||||||
@@ -147,8 +146,6 @@ 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)
|
||||||
|
|
||||||
@@ -163,8 +160,6 @@ 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')
|
||||||
@@ -179,10 +174,6 @@ 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)
|
||||||
|
|
||||||
@@ -191,12 +182,9 @@ 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')
|
||||||
@@ -212,10 +200,6 @@ 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)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ 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,
|
||||||
)
|
)
|
||||||
@@ -51,9 +50,8 @@ 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,
|
EventSettingsSerializer, ItemMetaPropertiesSerializer, SeatSerializer,
|
||||||
SeatBulkBlockInputSerializer, SeatSerializer, SubEventSerializer,
|
SubEventSerializer, TaxRuleSerializer,
|
||||||
TaxRuleSerializer,
|
|
||||||
)
|
)
|
||||||
from pretix.api.views import ConditionalListView
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -239,9 +237,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, operation in changed.items():
|
for module, action in changed.items():
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
'pretix.event.plugins.' + operation,
|
'pretix.event.plugins.' + action,
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
data={'plugin': module}
|
data={'plugin': module}
|
||||||
@@ -299,8 +297,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
|
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
|
||||||
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
|
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
|
||||||
if not new_event.all_sales_channels:
|
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
||||||
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
|
||||||
else:
|
else:
|
||||||
serializer.instance.set_defaults()
|
serializer.instance.set_defaults()
|
||||||
|
|
||||||
@@ -373,7 +370,7 @@ with scopes_disabled():
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SubEvent
|
model = SubEvent
|
||||||
fields = ['is_public', 'active', 'event__live']
|
fields = ['active', 'event__live']
|
||||||
|
|
||||||
def ends_after_qs(self, queryset, name, value):
|
def ends_after_qs(self, queryset, name, value):
|
||||||
expr = Q(
|
expr = Q(
|
||||||
@@ -746,24 +743,3 @@ 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)
|
|
||||||
|
|||||||
@@ -619,7 +619,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
def availability(self, request, *args, **kwargs):
|
def availability(self, request, *args, **kwargs):
|
||||||
quota = self.get_object()
|
quota = self.get_object()
|
||||||
|
|
||||||
qa = QuotaAvailability(full_results=True)
|
qa = QuotaAvailability()
|
||||||
qa.queue(quota)
|
qa.queue(quota)
|
||||||
qa.compute()
|
qa.compute()
|
||||||
avail = qa.results[quota]
|
avail = qa.results[quota]
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ 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
|
||||||
|
|
||||||
@@ -80,7 +79,6 @@ 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,7 +35,6 @@ 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
|
||||||
@@ -58,8 +57,7 @@ from pretix.api.serializers.order import (
|
|||||||
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
||||||
OrderPositionSerializer, OrderRefundCreateSerializer,
|
OrderPositionSerializer, OrderRefundCreateSerializer,
|
||||||
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
||||||
PrintLogSerializer, RevokedTicketSecretSerializer,
|
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
|
||||||
SimulatedOrderSerializer,
|
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.orderchange import (
|
from pretix.api.serializers.orderchange import (
|
||||||
BlockNameSerializer, OrderChangeOperationSerializer,
|
BlockNameSerializer, OrderChangeOperationSerializer,
|
||||||
@@ -68,7 +66,6 @@ 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,
|
||||||
@@ -78,7 +75,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, PrintLog, QuestionAnswer, RevokedTicketSecret,
|
BlockedTicketSecret, 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
|
||||||
@@ -99,6 +96,7 @@ 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
|
||||||
|
|
||||||
@@ -216,7 +214,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', 'cancellation_date')
|
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
|
||||||
filterset_class = OrderFilter
|
filterset_class = OrderFilter
|
||||||
lookup_field = 'code'
|
lookup_field = 'code'
|
||||||
|
|
||||||
@@ -261,7 +259,6 @@ 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')
|
||||||
)),
|
)),
|
||||||
@@ -283,7 +280,6 @@ 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',
|
||||||
@@ -602,7 +598,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
||||||
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
|
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
|
||||||
)
|
)
|
||||||
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'user_paid', 'True') or not invoice_qualified(order):
|
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'True') or not invoice_qualified(order):
|
||||||
return Response(
|
return Response(
|
||||||
{'detail': _('You cannot generate an invoice for this order.')},
|
{'detail': _('You cannot generate an invoice for this order.')},
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
@@ -647,8 +643,6 @@ 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
|
||||||
)
|
)
|
||||||
@@ -1099,7 +1093,6 @@ 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')
|
||||||
@@ -1143,7 +1136,6 @@ 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'
|
||||||
@@ -1231,10 +1223,9 @@ 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': gross_formatted,
|
'gross_formatted': money_filter(price.gross, self.request.event.currency, hide_currency=True),
|
||||||
'net': price.net,
|
'net': price.net,
|
||||||
'rate': price.rate,
|
'rate': price.rate,
|
||||||
'name': str(price.name),
|
'name': str(price.name),
|
||||||
@@ -1263,34 +1254,6 @@ 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=_("Email"), max_length=254,
|
('email', forms.EmailField(label=_("E-mail"), 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)),
|
||||||
@@ -222,15 +222,3 @@ class HistoryPasswordValidator:
|
|||||||
user.historic_passwords.filter(
|
user.historic_passwords.filter(
|
||||||
pk__in=user.historic_passwords.order_by("-created")[self.history_length:].values_list("pk", flat=True),
|
pk__in=user.historic_passwords.order_by("-created")[self.history_length:].values_list("pk", flat=True),
|
||||||
).delete()
|
).delete()
|
||||||
|
|
||||||
|
|
||||||
def has_event_access_permission(request, permission='can_change_event_settings'):
|
|
||||||
return (
|
|
||||||
request.user.is_authenticated and
|
|
||||||
request.user.has_event_permission(request.organizer, request.event, permission, request=request)
|
|
||||||
) or (
|
|
||||||
getattr(request, 'event_access_user', None) and
|
|
||||||
request.event_access_user.is_authenticated and
|
|
||||||
request.event_access_user.has_event_permission(request.organizer, request.event, permission,
|
|
||||||
session_key=request.event_access_parent_session_key)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ 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
|
||||||
@@ -69,7 +68,7 @@ def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
|||||||
|
|
||||||
class BaseHTMLMailRenderer:
|
class BaseHTMLMailRenderer:
|
||||||
"""
|
"""
|
||||||
This is the base class for all HTML email renderers.
|
This is the base class for all HTML e-mail renderers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, event: Event, organizer=None):
|
def __init__(self, event: Event, organizer=None):
|
||||||
@@ -80,7 +79,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, context=None) -> str:
|
position=None) -> str:
|
||||||
"""
|
"""
|
||||||
This method should generate the HTML part of the email.
|
This method should generate the HTML part of the email.
|
||||||
|
|
||||||
@@ -89,7 +88,6 @@ 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()
|
||||||
@@ -136,10 +134,8 @@ 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, context) -> str:
|
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> 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'),
|
||||||
_('Email'),
|
_('E-mail'),
|
||||||
_('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'),
|
||||||
_('Email address'),
|
_('E-mail 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'),
|
||||||
_('Email address'),
|
_('E-mail 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(_('Email address verified'))
|
headers.append(_('E-mail 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'),
|
||||||
_('Email address verified'),
|
_('E-mail 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, id_) -> str:
|
def format_output(self, rendered_widgets) -> 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, id_)
|
return super().format_output(rendered_widgets)
|
||||||
|
|
||||||
|
|
||||||
class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
|
class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
|
||||||
def format_output(self, rendered_widgets, id_) -> str:
|
def format_output(self, rendered_widgets) -> 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, id_)
|
return super().format_output(rendered_widgets)
|
||||||
|
|
||||||
|
|
||||||
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=_('Email'),
|
label=_('E-mail'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ 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
|
||||||
@@ -78,7 +77,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 ask_for_vat_id
|
from pretix.base.models.tax import VAT_ID_COUNTRIES, 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,
|
||||||
)
|
)
|
||||||
@@ -277,10 +276,6 @@ 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
|
||||||
|
|
||||||
@@ -607,7 +602,6 @@ 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)
|
||||||
|
|
||||||
@@ -682,7 +676,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=self.attendee_addresses_required,
|
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||||
label=_('Address'),
|
label=_('Address'),
|
||||||
widget=forms.Textarea(attrs={
|
widget=forms.Textarea(attrs={
|
||||||
'rows': 2,
|
'rows': 2,
|
||||||
@@ -692,7 +686,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=False,
|
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||||
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),
|
||||||
@@ -701,7 +695,7 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
add_fields['city'] = forms.CharField(
|
add_fields['city'] = forms.CharField(
|
||||||
required=False,
|
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||||
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),
|
||||||
@@ -713,12 +707,11 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
add_fields['country'] = CountryField(
|
add_fields['country'] = CountryField(
|
||||||
countries=CachedCountries
|
countries=CachedCountries
|
||||||
).formfield(
|
).formfield(
|
||||||
required=self.attendee_addresses_required,
|
required=event.settings.attendee_addresses_required and not self.all_optional,
|
||||||
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'))]
|
||||||
@@ -953,9 +946,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, all_optional=not self.attendee_addresses_required)
|
self.cleaned_data = d = validate_address(d, True)
|
||||||
|
|
||||||
if d.get('street') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||||
if not d.get('state'):
|
if not d.get('state'):
|
||||||
self.add_error('state', _('This field is required.'))
|
self.add_error('state', _('This field is required.'))
|
||||||
|
|
||||||
@@ -1012,7 +1005,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={
|
||||||
@@ -1025,27 +1018,16 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
'autocomplete': 'address-level2',
|
'autocomplete': 'address-level2',
|
||||||
}),
|
}),
|
||||||
'company': forms.TextInput(attrs={
|
'company': forms.TextInput(attrs={
|
||||||
|
'data-display-dependency': '#id_is_business_1',
|
||||||
'autocomplete': 'organization',
|
'autocomplete': 'organization',
|
||||||
}),
|
}),
|
||||||
'vat_id': forms.TextInput(),
|
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-with-vat-id': ','.join(VAT_ID_COUNTRIES)}),
|
||||||
'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)
|
||||||
@@ -1057,11 +1039,7 @@ 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'input[name="{self.add_prefix("is_business")}"][value="business"]'
|
|
||||||
self.fields["vat_id"].widget.attrs["data-display-dependency"] = f'input[name="{self.add_prefix("is_business")}"][value="business"]'
|
|
||||||
|
|
||||||
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([
|
||||||
@@ -1077,7 +1055,6 @@ 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 ''
|
||||||
@@ -1106,22 +1083,18 @@ 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 self.address_required or self.all_optional:
|
if not event.settings.invoice_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 self.company_required and not self.all_optional:
|
elif event.settings.invoice_address_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)
|
||||||
@@ -1138,18 +1111,17 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
label=_('Name'),
|
label=_('Name'),
|
||||||
initial=self.instance.name_parts,
|
initial=self.instance.name_parts,
|
||||||
)
|
)
|
||||||
if self.address_required and not self.company_required and not self.all_optional:
|
if event.settings.invoice_address_required and not event.settings.invoice_address_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'] = f'input[name="{self.add_prefix("is_business")}"][value="individual"]'
|
self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_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'] = f'input[name="{self.add_prefix("is_business")}"][value="business"]'
|
self.fields['company'].widget.attrs['data-required-if'] = '#id_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']
|
||||||
|
|
||||||
if event.settings.invoice_address_custom_field:
|
if event.settings.invoice_address_custom_field:
|
||||||
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
|
self.fields['custom_field'].label = event.settings.invoice_address_custom_field
|
||||||
self.fields['custom_field'].help_text = event.settings.invoice_address_custom_field_helptext
|
|
||||||
else:
|
else:
|
||||||
del self.fields['custom_field']
|
del self.fields['custom_field']
|
||||||
|
|
||||||
@@ -1162,19 +1134,16 @@ 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.address_validation and self.address_required and not self.all_optional:
|
if self.event.settings.invoice_address_required:
|
||||||
if data.get('is_business') and not data.get('company'):
|
if data.get('is_business') and not data.get('company'):
|
||||||
raise ValidationError({"company": _('You need to provide a company name.')})
|
raise ValidationError(_('You need to provide a company name.'))
|
||||||
if not data.get('is_business') and name_parts_is_empty(data.get('name_parts', {})):
|
if not data.get('is_business') and not 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
|
||||||
@@ -1186,7 +1155,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 name_parts_is_empty(data.get('name_parts', {})):
|
) and len(data.get('name_parts', {})) == 1:
|
||||||
# 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 email address. "
|
'duplicate_identifier': _("There already is an account associated with this e-mail address. "
|
||||||
"Please choose a different one."),
|
"Please choose a different one."),
|
||||||
'pw_current': _("Please enter your current password if you want to change your email address "
|
'pw_current': _("Please enter your current password if you want to change your e-mail "
|
||||||
"or password."),
|
"address 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=set(tags) if tags else set()
|
tags=tags or []
|
||||||
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
||||||
|
|
||||||
|
|
||||||
@@ -388,15 +388,6 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
except:
|
except:
|
||||||
logger.exception("Can not resize image")
|
logger.exception("Can not resize image")
|
||||||
pass
|
pass
|
||||||
try:
|
|
||||||
# Valid ZUGFeRD invoices must be compliant with PDF/A-3. pretix-zugferd ensures this by passing them
|
|
||||||
# through ghost script. Unfortunately, if the logo contains transparency, this will still fail.
|
|
||||||
# I was unable to figure out a way to fix this in GhostScript, so the easy fix is to remove the
|
|
||||||
# transparency, as our invoices always have a white background anyways.
|
|
||||||
ir.remove_transparency()
|
|
||||||
except:
|
|
||||||
logger.exception("Can not remove transparency from logo")
|
|
||||||
pass
|
|
||||||
canvas.drawImage(ir,
|
canvas.drawImage(ir,
|
||||||
self.logo_left,
|
self.logo_left,
|
||||||
self.pagesize[1] - self.logo_height - self.logo_top,
|
self.pagesize[1] - self.logo_height - self.logo_top,
|
||||||
@@ -470,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=set()).strip()
|
txt = bleach.clean(txt, tags=[]).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)
|
||||||
|
|
||||||
@@ -784,7 +775,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
for idx, gross in grossvalue_map.items():
|
for idx, gross in grossvalue_map.items():
|
||||||
rate, name = idx
|
rate, name = idx
|
||||||
if rate == 0 and gross == 0:
|
if rate == 0:
|
||||||
continue
|
continue
|
||||||
tax = taxvalue_map[idx]
|
tax = taxvalue_map[idx]
|
||||||
tdata.append([
|
tdata.append([
|
||||||
@@ -801,7 +792,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return localize(val) + ' ' + self.invoice.foreign_currency_display
|
return localize(val) + ' ' + self.invoice.foreign_currency_display
|
||||||
|
|
||||||
if any(rate != 0 and gross != 0 for (rate, name), gross in grossvalue_map.items()) and has_taxes:
|
if len(tdata) > 1 and has_taxes:
|
||||||
colwidths = [a * doc.width for a in (.25, .15, .15, .15, .3)]
|
colwidths = [a * doc.width for a in (.25, .15, .15, .15, .3)]
|
||||||
table = Table(tdata, colWidths=colwidths, repeatRows=2, hAlign=TA_LEFT)
|
table = Table(tdata, colWidths=colwidths, repeatRows=2, hAlign=TA_LEFT)
|
||||||
table.setStyle(TableStyle(tstyledata))
|
table.setStyle(TableStyle(tstyledata))
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from collections import defaultdict
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.html import format_html
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from pretix.base.signals import EventPluginRegistry
|
|
||||||
|
|
||||||
|
|
||||||
def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
|
|
||||||
if a_map:
|
|
||||||
if 'href' not in a_map:
|
|
||||||
a_map['val'] = format_html('<i>{val}</i>', **a_map)
|
|
||||||
elif is_active:
|
|
||||||
a_map['val'] = format_html('<a href="{href}">{val}</a>', **a_map)
|
|
||||||
elif event and plugin_name:
|
|
||||||
a_map['val'] = format_html(
|
|
||||||
'<i>{val}</i> <a href="{plugin_href}">'
|
|
||||||
'<span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span></a>',
|
|
||||||
**a_map,
|
|
||||||
errmes=_("The relevant plugin is currently not active. To activate it, click here to go to the plugin settings."),
|
|
||||||
plugin_href=reverse('control:event.settings.plugins', kwargs={
|
|
||||||
'organizer': event.organizer.slug,
|
|
||||||
'event': event.slug,
|
|
||||||
}) + '#plugin_' + plugin_name,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
a_map['val'] = format_html(
|
|
||||||
'<i>{val}</i> <span data-toggle="tooltip" title="{errmes}" class="fa fa-warning fa-fw"></span>',
|
|
||||||
**a_map,
|
|
||||||
errmes=_("The relevant plugin is currently not active."),
|
|
||||||
)
|
|
||||||
return format_html(wrapper, **a_map)
|
|
||||||
|
|
||||||
|
|
||||||
class LogEntryTypeRegistry(EventPluginRegistry):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__({'action_type': lambda o: getattr(o, 'action_type')})
|
|
||||||
|
|
||||||
def register(self, *objs):
|
|
||||||
for obj in objs:
|
|
||||||
if not isinstance(obj, LogEntryType):
|
|
||||||
raise TypeError('Entries must be derived from LogEntryType')
|
|
||||||
|
|
||||||
if obj.__module__.startswith('pretix.base.'):
|
|
||||||
raise TypeError('Must not register base classes, only derived ones')
|
|
||||||
|
|
||||||
return super().register(*objs)
|
|
||||||
|
|
||||||
def new_from_dict(self, data):
|
|
||||||
"""
|
|
||||||
Register multiple instance of a `LogEntryType` class with different `action_type`
|
|
||||||
and plain text strings, as given by the items of the specified data dictionary.
|
|
||||||
|
|
||||||
This method is designed to be used as a decorator as follows:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@log_entry_types.new_from_dict({
|
|
||||||
'pretix.event.item.added': _('The product has been created.'),
|
|
||||||
'pretix.event.item.changed': _('The product has been changed.'),
|
|
||||||
# ...
|
|
||||||
})
|
|
||||||
class CoreItemLogEntryType(ItemLogEntryType):
|
|
||||||
# ...
|
|
||||||
|
|
||||||
:param data: action types and descriptions
|
|
||||||
``{"some_action_type": "Plain text description", ...}``
|
|
||||||
"""
|
|
||||||
def reg(clz):
|
|
||||||
for action_type, plain in data.items():
|
|
||||||
self.register(clz(action_type=action_type, plain=plain))
|
|
||||||
return clz
|
|
||||||
return reg
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Registry for LogEntry types.
|
|
||||||
|
|
||||||
Each entry in this registry should be an instance of a subclass of ``LogEntryType``.
|
|
||||||
They are annotated with their ``action_type`` and the defining ``plugin``.
|
|
||||||
"""
|
|
||||||
log_entry_types = LogEntryTypeRegistry()
|
|
||||||
|
|
||||||
|
|
||||||
class LogEntryType:
|
|
||||||
"""
|
|
||||||
Base class for a type of LogEntry, identified by its action_type.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, action_type=None, plain=None):
|
|
||||||
if action_type:
|
|
||||||
self.action_type = action_type
|
|
||||||
if plain:
|
|
||||||
self.plain = plain
|
|
||||||
|
|
||||||
def display(self, logentry, data):
|
|
||||||
"""
|
|
||||||
Returns the message to be displayed for a given logentry of this type.
|
|
||||||
|
|
||||||
:return: `str` or `LazyI18nString`
|
|
||||||
"""
|
|
||||||
if hasattr(self, 'plain'):
|
|
||||||
plain = str(self.plain)
|
|
||||||
if '{' in plain:
|
|
||||||
data = defaultdict(lambda: '?', data)
|
|
||||||
return plain.format_map(data)
|
|
||||||
else:
|
|
||||||
return plain
|
|
||||||
|
|
||||||
def get_object_link_info(self, logentry) -> Optional[dict]:
|
|
||||||
"""
|
|
||||||
Return information to generate a link to the `content_object` of a given log entry.
|
|
||||||
|
|
||||||
Not implemented in the base class, causing the object link to be omitted.
|
|
||||||
|
|
||||||
:return: Dictionary with the keys ``href`` (URL to view/edit the object) and
|
|
||||||
``val`` (text for the anchor element)
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_object_link(self, logentry):
|
|
||||||
a_map = self.get_object_link_info(logentry)
|
|
||||||
return make_link(a_map, self.object_link_wrapper)
|
|
||||||
|
|
||||||
object_link_wrapper = '{val}'
|
|
||||||
|
|
||||||
def shred_pii(self, logentry):
|
|
||||||
"""
|
|
||||||
To be used for shredding personally identified information contained in the data field of a LogEntry of this
|
|
||||||
type.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class NoOpShredderMixin:
|
|
||||||
def shred_pii(self, logentry):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ClearDataShredderMixin:
|
|
||||||
def shred_pii(self, logentry):
|
|
||||||
logentry.data = None
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
|
||||||
|
|
||||||
from pretix.base.models import (
|
|
||||||
Discount, Item, ItemCategory, Order, Question, Quota, SubEvent, TaxRule,
|
|
||||||
Voucher,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .logentrytype_registry import ( # noqa
|
|
||||||
ClearDataShredderMixin, LogEntryType, NoOpShredderMixin, log_entry_types,
|
|
||||||
make_link, LogEntryTypeRegistry,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EventLogEntryType(LogEntryType):
|
|
||||||
"""
|
|
||||||
Base class for any `LogEntry` type whose `content_object` is either an `Event` itself or belongs to a specific `Event`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_object_link_info(self, logentry) -> Optional[dict]:
|
|
||||||
if hasattr(self, 'object_link_viewname'):
|
|
||||||
content = logentry.content_object
|
|
||||||
if not content:
|
|
||||||
if logentry.content_type_id:
|
|
||||||
return {
|
|
||||||
'val': _('(deleted)'),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
if hasattr(self, 'content_type') and not isinstance(content, self.content_type):
|
|
||||||
return
|
|
||||||
|
|
||||||
return {
|
|
||||||
'href': reverse(self.object_link_viewname, kwargs={
|
|
||||||
'event': logentry.event.slug,
|
|
||||||
'organizer': logentry.event.organizer.slug,
|
|
||||||
**self.object_link_args(content),
|
|
||||||
}),
|
|
||||||
'val': self.object_link_display_name(logentry.content_object),
|
|
||||||
}
|
|
||||||
|
|
||||||
def object_link_args(self, content_object):
|
|
||||||
"""Return the kwargs for the url used in a link to content_object."""
|
|
||||||
if hasattr(self, 'object_link_argname'):
|
|
||||||
return {self.object_link_argname: content_object.pk}
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def object_link_display_name(self, content_object):
|
|
||||||
"""Return the display name to refer to content_object in the user interface."""
|
|
||||||
return str(content_object)
|
|
||||||
|
|
||||||
|
|
||||||
class OrderLogEntryType(EventLogEntryType):
|
|
||||||
object_link_wrapper = _('Order {val}')
|
|
||||||
object_link_viewname = 'control:event.order'
|
|
||||||
content_type = Order
|
|
||||||
|
|
||||||
def object_link_args(self, order):
|
|
||||||
return {'code': order.code}
|
|
||||||
|
|
||||||
def object_link_display_name(self, order):
|
|
||||||
return order.code
|
|
||||||
|
|
||||||
|
|
||||||
class VoucherLogEntryType(EventLogEntryType):
|
|
||||||
object_link_wrapper = _('Voucher {val}…')
|
|
||||||
object_link_viewname = 'control:event.voucher'
|
|
||||||
object_link_argname = 'voucher'
|
|
||||||
content_type = Voucher
|
|
||||||
|
|
||||||
def object_link_display_name(self, voucher):
|
|
||||||
if len(voucher.code) > 6:
|
|
||||||
return voucher.code[:6] + "…"
|
|
||||||
return voucher.code
|
|
||||||
|
|
||||||
|
|
||||||
class ItemLogEntryType(EventLogEntryType):
|
|
||||||
object_link_wrapper = _('Product {val}')
|
|
||||||
object_link_viewname = 'control:event.item'
|
|
||||||
object_link_argname = 'item'
|
|
||||||
content_type = Item
|
|
||||||
|
|
||||||
|
|
||||||
class SubEventLogEntryType(EventLogEntryType):
|
|
||||||
object_link_wrapper = pgettext_lazy('subevent', 'Date {val}')
|
|
||||||
object_link_viewname = 'control:event.subevent'
|
|
||||||
object_link_argname = 'subevent'
|
|
||||||
content_type = SubEvent
|
|
||||||
|
|
||||||
|
|
||||||
class QuotaLogEntryType(EventLogEntryType):
|
|
||||||
object_link_wrapper = _('Quota {val}')
|
|
||||||
object_link_viewname = 'control:event.items.quotas.show'
|
|
||||||
object_link_argname = 'quota'
|
|
||||||
content_type = Quota
|
|
||||||
|
|
||||||
|
|
||||||
class DiscountLogEntryType(EventLogEntryType):
|
|
||||||
object_link_wrapper = _('Discount {val}')
|
|
||||||
object_link_viewname = 'control:event.items.discounts.edit'
|
|
||||||
object_link_argname = 'discount'
|
|
||||||
content_type = Discount
|
|
||||||
|
|
||||||
|
|
||||||
class ItemCategoryLogEntryType(EventLogEntryType):
|
|
||||||
object_link_wrapper = _('Category {val}')
|
|
||||||
object_link_viewname = 'control:event.items.categories.edit'
|
|
||||||
object_link_argname = 'category'
|
|
||||||
content_type = ItemCategory
|
|
||||||
|
|
||||||
|
|
||||||
class QuestionLogEntryType(EventLogEntryType):
|
|
||||||
object_link_wrapper = _('Question {val}')
|
|
||||||
object_link_viewname = 'control:event.items.questions.show'
|
|
||||||
object_link_argname = 'question'
|
|
||||||
content_type = Question
|
|
||||||
|
|
||||||
|
|
||||||
class TaxRuleLogEntryType(EventLogEntryType):
|
|
||||||
object_link_wrapper = _('Tax rule {val}')
|
|
||||||
object_link_viewname = 'control:event.settings.tax.edit'
|
|
||||||
object_link_argname = 'rule'
|
|
||||||
content_type = TaxRule
|
|
||||||
@@ -36,7 +36,6 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.dispatch.dispatcher import NO_RECEIVERS
|
from django.dispatch.dispatcher import NO_RECEIVERS
|
||||||
|
|
||||||
@@ -58,8 +57,6 @@ class Command(BaseCommand):
|
|||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
verbosity = int(options['verbosity'])
|
verbosity = int(options['verbosity'])
|
||||||
|
|
||||||
cache.set("pretix_runperiodic_executed", True, 3600 * 12)
|
|
||||||
|
|
||||||
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
|
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -81,7 +78,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(err, KeyboardInterrupt):
|
if isinstance(Exception, 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,16 +37,6 @@ 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()
|
||||||
@@ -69,7 +59,6 @@ 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
|
||||||
@@ -86,7 +75,6 @@ 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
|
||||||
@@ -126,7 +114,6 @@ 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='Email', null=True,
|
('email', models.EmailField(max_length=191, blank=True, unique=True, verbose_name='E-mail', 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,7 +9,6 @@ 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
|
||||||
@@ -26,14 +25,7 @@ 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
|
||||||
try:
|
user.password = make_password('admin')
|
||||||
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()
|
||||||
|
|
||||||
|
|
||||||
@@ -56,7 +48,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='Email')),
|
('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, unique=True, verbose_name='E-mail')),
|
||||||
('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')),
|
||||||
@@ -240,7 +232,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='Email')),
|
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
|
||||||
('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='Email')),
|
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-mail')),
|
||||||
('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='Email address')),
|
('email', models.EmailField(max_length=254, verbose_name='E-mail 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')),
|
||||||
|
|||||||
@@ -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='Email address')),
|
('email', models.EmailField(max_length=254, verbose_name='E-mail 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')),
|
||||||
|
|||||||
@@ -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', 'Email')], max_length=255)),
|
('method', models.CharField(choices=[('mail', 'E-mail')], 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', 'Email')], max_length=255)),
|
('method', models.CharField(choices=[('mail', 'E-mail')], 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)),
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
# Generated by Django 4.2.11 on 2024-05-27 13:19
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import pretix.base.models.orders
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixbase", "0270_historicpassword"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="itemcategory",
|
|
||||||
name="cross_selling_condition",
|
|
||||||
field=models.CharField(null=True, max_length=10),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="itemcategory",
|
|
||||||
name="cross_selling_mode",
|
|
||||||
field=models.CharField(null=True, max_length=5),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="itemcategory",
|
|
||||||
name="cross_selling_match_products",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
related_name="matched_by_cross_selling_categories", to="pretixbase.item"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
# 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",),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# 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",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# 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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 4.2.17 on 2025-01-13 14:55
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixbase", "0274_tax_codes"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="question",
|
|
||||||
name="valid_number_max",
|
|
||||||
field=models.DecimalField(decimal_places=6, max_digits=30, null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="question",
|
|
||||||
name="valid_number_min",
|
|
||||||
field=models.DecimalField(decimal_places=6, max_digits=30, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 4.2.16 on 2025-01-23 11:27
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixbase", "0275_alter_question_valid_number_max_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="item",
|
|
||||||
name="hidden_if_item_available_mode",
|
|
||||||
field=models.CharField(default="hide", max_length=16),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -256,9 +256,6 @@ 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('Email address')
|
verbose_name = gettext_lazy('E-mail 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 email address')
|
verbose_name = gettext_lazy('Attendee e-mail address')
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
if value:
|
if value:
|
||||||
@@ -441,7 +441,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -585,7 +584,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(sales_channel=previous_values.get('sales_channel')) or value in self._cached:
|
if not value.is_available() 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)
|
||||||
@@ -754,11 +753,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=_('Email'), max_length=190)
|
verbose_name=_('E-mail'), 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,
|
||||||
@@ -571,23 +571,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
|
|
||||||
def get_session_auth_hash(self):
|
def get_session_auth_hash(self):
|
||||||
"""
|
"""
|
||||||
Return an HMAC that needs to be the same throughout the session, used e.g. for forced
|
Return an HMAC that needs to
|
||||||
logout after every password change.
|
|
||||||
"""
|
|
||||||
return self._get_session_auth_hash(secret=settings.SECRET_KEY)
|
|
||||||
|
|
||||||
def get_session_auth_fallback_hash(self):
|
|
||||||
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
|
|
||||||
yield self._get_session_auth_hash(secret=fallback_secret)
|
|
||||||
|
|
||||||
def _get_session_auth_hash(self, secret):
|
|
||||||
"""
|
|
||||||
"""
|
"""
|
||||||
key_salt = "pretix.base.models.User.get_session_auth_hash"
|
key_salt = "pretix.base.models.User.get_session_auth_hash"
|
||||||
payload = self.password
|
payload = self.password
|
||||||
payload += self.email
|
payload += self.email
|
||||||
payload += self.session_token
|
payload += self.session_token
|
||||||
return salted_hmac(key_salt, payload, secret=secret).hexdigest()
|
return salted_hmac(key_salt, payload).hexdigest()
|
||||||
|
|
||||||
def update_session_token(self):
|
def update_session_token(self):
|
||||||
self.session_token = generate_session_token()
|
self.session_token = generate_session_token()
|
||||||
|
|||||||
@@ -99,6 +99,14 @@ 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')
|
||||||
@@ -133,7 +141,7 @@ class CheckinList(LoggedModel):
|
|||||||
return self.positions_query(ignore_status=False)
|
return self.positions_query(ignore_status=False)
|
||||||
|
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def _filter_positions_inside(self, qs, at_time=None):
|
def positions_inside_query(self, ignore_status=False, at_time=None):
|
||||||
if at_time is None:
|
if at_time is None:
|
||||||
c_q = []
|
c_q = []
|
||||||
else:
|
else:
|
||||||
@@ -141,7 +149,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 = qs.annotate(
|
qs = self.positions_query(ignore_status=ignore_status).annotate(
|
||||||
last_entry=Subquery(
|
last_entry=Subquery(
|
||||||
Checkin.objects.filter(
|
Checkin.objects.filter(
|
||||||
*c_q,
|
*c_q,
|
||||||
@@ -194,7 +202,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 qs.filter(
|
return self.positions_query(ignore_status=ignore_status).filter(
|
||||||
pk__in=RawSQL(
|
pk__in=RawSQL(
|
||||||
f"""
|
f"""
|
||||||
SELECT "position_id"
|
SELECT "position_id"
|
||||||
@@ -206,10 +214,6 @@ 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=_('Email'), max_length=190)
|
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('E-mail'), 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)
|
||||||
@@ -219,24 +219,13 @@ class Customer(LoggedModel):
|
|||||||
return is_password_usable(self.password)
|
return is_password_usable(self.password)
|
||||||
|
|
||||||
def get_session_auth_hash(self):
|
def get_session_auth_hash(self):
|
||||||
"""
|
|
||||||
Return an HMAC that needs to be the same throughout the session, used e.g. for forced
|
|
||||||
logout after every password change.
|
|
||||||
"""
|
|
||||||
return self._get_session_auth_hash(secret=settings.SECRET_KEY)
|
|
||||||
|
|
||||||
def get_session_auth_fallback_hash(self):
|
|
||||||
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
|
|
||||||
yield self._get_session_auth_hash(secret=fallback_secret)
|
|
||||||
|
|
||||||
def _get_session_auth_hash(self, secret):
|
|
||||||
"""
|
"""
|
||||||
Return an HMAC of the password field.
|
Return an HMAC of the password field.
|
||||||
"""
|
"""
|
||||||
key_salt = "pretix.base.models.customers.Customer.get_session_auth_hash"
|
key_salt = "pretix.base.models.customers.Customer.get_session_auth_hash"
|
||||||
payload = self.password
|
payload = self.password
|
||||||
payload += self.email
|
payload += self.email
|
||||||
return salted_hmac(key_salt, payload, secret=secret).hexdigest()
|
return salted_hmac(key_salt, payload).hexdigest()
|
||||||
|
|
||||||
def get_email_context(self):
|
def get_email_context(self):
|
||||||
from pretix.base.settings import get_name_parts_localized
|
from pretix.base.settings import get_name_parts_localized
|
||||||
@@ -392,7 +381,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', _('Email address')),
|
('email', _('E-mail address')),
|
||||||
('phone', _('Phone number')),
|
('phone', _('Phone number')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -160,6 +161,7 @@ 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
|
||||||
|
|||||||
@@ -20,11 +20,11 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from math import ceil, inf
|
from math import ceil
|
||||||
from typing import Dict
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
@@ -36,8 +36,6 @@ from django_scopes import ScopedManager
|
|||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
|
|
||||||
PositionInfo = namedtuple('PositionInfo', ['item_id', 'subevent_id', 'line_price_gross', 'is_addon_to', 'voucher_discount'])
|
|
||||||
|
|
||||||
|
|
||||||
class Discount(LoggedModel):
|
class Discount(LoggedModel):
|
||||||
SUBEVENT_MODE_MIXED = 'mixed'
|
SUBEVENT_MODE_MIXED = 'mixed'
|
||||||
@@ -247,26 +245,22 @@ class Discount(LoggedModel):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
|
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result):
|
||||||
if self.condition_min_value and sum(positions[idx].line_price_gross for idx in condition_idx_group) < self.condition_min_value:
|
if self.condition_min_value and sum(positions[idx][2] for idx in condition_idx_group) < self.condition_min_value:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.condition_min_count or self.benefit_only_apply_to_cheapest_n_matches:
|
if self.condition_min_count or self.benefit_only_apply_to_cheapest_n_matches:
|
||||||
raise ValueError('Validation invariant violated.')
|
raise ValueError('Validation invariant violated.')
|
||||||
|
|
||||||
for idx in benefit_idx_group:
|
for idx in benefit_idx_group:
|
||||||
previous_price = positions[idx].line_price_gross
|
previous_price = positions[idx][2]
|
||||||
new_price = round_decimal(
|
new_price = round_decimal(
|
||||||
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
|
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
|
||||||
self.event.currency,
|
self.event.currency,
|
||||||
)
|
)
|
||||||
result[idx] = new_price
|
result[idx] = new_price
|
||||||
|
|
||||||
if collect_potential_discounts is not None:
|
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result):
|
||||||
for idx in condition_idx_group:
|
|
||||||
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
|
|
||||||
|
|
||||||
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
|
|
||||||
if len(condition_idx_group) < self.condition_min_count:
|
if len(condition_idx_group) < self.condition_min_count:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -274,53 +268,23 @@ class Discount(LoggedModel):
|
|||||||
raise ValueError('Validation invariant violated.')
|
raise ValueError('Validation invariant violated.')
|
||||||
|
|
||||||
if self.benefit_only_apply_to_cheapest_n_matches:
|
if self.benefit_only_apply_to_cheapest_n_matches:
|
||||||
# sort by line_price
|
if not self.condition_min_count:
|
||||||
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
|
raise ValueError('Validation invariant violated.')
|
||||||
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
|
|
||||||
|
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
|
||||||
|
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
|
||||||
|
|
||||||
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
|
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
|
||||||
# want to match multiples of 3
|
# want to match multiples of 3
|
||||||
|
n_groups = min(len(condition_idx_group) // self.condition_min_count, ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches))
|
||||||
# how many discount applications are allowed according to condition products in cart
|
|
||||||
possible_applications_cond = len(condition_idx_group) // self.condition_min_count
|
|
||||||
|
|
||||||
# how many discount applications are possible according to benefitting products in cart
|
|
||||||
possible_applications_benefit = ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches)
|
|
||||||
|
|
||||||
n_groups = min(possible_applications_cond, possible_applications_benefit)
|
|
||||||
consume_idx = condition_idx_group[:n_groups * self.condition_min_count]
|
consume_idx = condition_idx_group[:n_groups * self.condition_min_count]
|
||||||
benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches]
|
benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches]
|
||||||
|
|
||||||
if collect_potential_discounts is not None:
|
|
||||||
if n_groups * self.benefit_only_apply_to_cheapest_n_matches > len(benefit_idx_group):
|
|
||||||
# partially used discount ("for each 1 ticket you buy, get 50% on 2 t-shirts", cart content: 1 ticket
|
|
||||||
# but only 1 t-shirt) -> 1 shirt definitiv potential discount
|
|
||||||
for idx in consume_idx:
|
|
||||||
collect_potential_discounts[idx] = [
|
|
||||||
(self, n_groups * self.benefit_only_apply_to_cheapest_n_matches - len(benefit_idx_group), -1, subevent_id)
|
|
||||||
]
|
|
||||||
|
|
||||||
if possible_applications_cond * self.benefit_only_apply_to_cheapest_n_matches > len(benefit_idx_group):
|
|
||||||
# unused discount ("for each 1 ticket you buy, get 50% on 2 t-shirts", cart content: 1 ticket
|
|
||||||
# but 0 t-shirts) -> 2 shirt maybe potential discount (if the 1 ticket is not consumed by a later discount)
|
|
||||||
for i, idx in enumerate(condition_idx_group[
|
|
||||||
n_groups * self.condition_min_count:
|
|
||||||
possible_applications_cond * self.condition_min_count
|
|
||||||
]):
|
|
||||||
collect_potential_discounts[idx] += [
|
|
||||||
(self, self.benefit_only_apply_to_cheapest_n_matches, i // self.condition_min_count, subevent_id)
|
|
||||||
]
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
consume_idx = condition_idx_group
|
consume_idx = condition_idx_group
|
||||||
benefit_idx = benefit_idx_group
|
benefit_idx = benefit_idx_group
|
||||||
|
|
||||||
if collect_potential_discounts is not None:
|
|
||||||
for idx in consume_idx:
|
|
||||||
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
|
|
||||||
|
|
||||||
for idx in benefit_idx:
|
for idx in benefit_idx:
|
||||||
previous_price = positions[idx].line_price_gross
|
previous_price = positions[idx][2]
|
||||||
new_price = round_decimal(
|
new_price = round_decimal(
|
||||||
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
|
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
|
||||||
self.event.currency,
|
self.event.currency,
|
||||||
@@ -328,16 +292,15 @@ class Discount(LoggedModel):
|
|||||||
result[idx] = new_price
|
result[idx] = new_price
|
||||||
|
|
||||||
for idx in consume_idx:
|
for idx in consume_idx:
|
||||||
result.setdefault(idx, positions[idx].line_price_gross)
|
result.setdefault(idx, positions[idx][2])
|
||||||
|
|
||||||
def apply(self, positions: Dict[int, PositionInfo],
|
def apply(self, positions: Dict[int, Tuple[int, Optional[int], Decimal, bool, Decimal]]) -> Dict[int, Decimal]:
|
||||||
collect_potential_discounts=None) -> Dict[int, Decimal]:
|
|
||||||
"""
|
"""
|
||||||
Tries to apply this discount to a cart
|
Tries to apply this discount to a cart
|
||||||
|
|
||||||
:param positions: Dictionary mapping IDs to PositionInfo tuples.
|
:param positions: Dictionary mapping IDs to tuples of the form
|
||||||
|
``(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)``.
|
||||||
Bundled positions may not be included.
|
Bundled positions may not be included.
|
||||||
:param collect_potential_discounts: For detailed description, see pretix.base.services.pricing.apply_discounts
|
|
||||||
|
|
||||||
:return: A dictionary mapping keys from the input dictionary to new prices. All positions
|
:return: A dictionary mapping keys from the input dictionary to new prices. All positions
|
||||||
contained in this dictionary are considered "consumed" and should not be considered
|
contained in this dictionary are considered "consumed" and should not be considered
|
||||||
@@ -379,13 +342,13 @@ class Discount(LoggedModel):
|
|||||||
|
|
||||||
if self.subevent_mode == self.SUBEVENT_MODE_MIXED: # also applies to non-series events
|
if self.subevent_mode == self.SUBEVENT_MODE_MIXED: # also applies to non-series events
|
||||||
if self.condition_min_count:
|
if self.condition_min_count:
|
||||||
self._apply_min_count(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
|
self._apply_min_count(positions, condition_candidates, benefit_candidates, result)
|
||||||
else:
|
else:
|
||||||
self._apply_min_value(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
|
self._apply_min_value(positions, condition_candidates, benefit_candidates, result)
|
||||||
|
|
||||||
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
|
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
|
||||||
def key(idx):
|
def key(idx):
|
||||||
return positions[idx].subevent_id or 0
|
return positions[idx][1] or 0 # subevent_id
|
||||||
|
|
||||||
# Build groups of candidates with the same subevent, then apply our regular algorithm
|
# Build groups of candidates with the same subevent, then apply our regular algorithm
|
||||||
# to each group
|
# to each group
|
||||||
@@ -394,11 +357,11 @@ class Discount(LoggedModel):
|
|||||||
candidate_groups = [(k, list(g)) for k, g in _groups]
|
candidate_groups = [(k, list(g)) for k, g in _groups]
|
||||||
|
|
||||||
for subevent_id, g in candidate_groups:
|
for subevent_id, g in candidate_groups:
|
||||||
benefit_g = [idx for idx in benefit_candidates if positions[idx].subevent_id == subevent_id]
|
benefit_g = [idx for idx in benefit_candidates if positions[idx][1] == subevent_id]
|
||||||
if self.condition_min_count:
|
if self.condition_min_count:
|
||||||
self._apply_min_count(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
|
self._apply_min_count(positions, g, benefit_g, result)
|
||||||
else:
|
else:
|
||||||
self._apply_min_value(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
|
self._apply_min_value(positions, g, benefit_g, result)
|
||||||
|
|
||||||
elif self.subevent_mode == self.SUBEVENT_MODE_DISTINCT:
|
elif self.subevent_mode == self.SUBEVENT_MODE_DISTINCT:
|
||||||
if self.condition_min_value or not self.benefit_same_products:
|
if self.condition_min_value or not self.benefit_same_products:
|
||||||
@@ -414,9 +377,9 @@ class Discount(LoggedModel):
|
|||||||
# Build a list of subevent IDs in descending order of frequency
|
# Build a list of subevent IDs in descending order of frequency
|
||||||
subevent_to_idx = defaultdict(list)
|
subevent_to_idx = defaultdict(list)
|
||||||
for idx, p in positions.items():
|
for idx, p in positions.items():
|
||||||
subevent_to_idx[p.subevent_id].append(idx)
|
subevent_to_idx[p[1]].append(idx)
|
||||||
for v in subevent_to_idx.values():
|
for v in subevent_to_idx.values():
|
||||||
v.sort(key=lambda idx: positions[idx].line_price_gross)
|
v.sort(key=lambda idx: positions[idx][2])
|
||||||
subevent_order = sorted(list(subevent_to_idx.keys()), key=lambda s: len(subevent_to_idx[s]), reverse=True)
|
subevent_order = sorted(list(subevent_to_idx.keys()), key=lambda s: len(subevent_to_idx[s]), reverse=True)
|
||||||
|
|
||||||
# Build groups of exactly condition_min_count distinct subevents
|
# Build groups of exactly condition_min_count distinct subevents
|
||||||
@@ -431,7 +394,7 @@ class Discount(LoggedModel):
|
|||||||
l = [ll for ll in l if ll in condition_candidates and ll not in current_group]
|
l = [ll for ll in l if ll in condition_candidates and ll not in current_group]
|
||||||
if cardinality and len(l) != cardinality:
|
if cardinality and len(l) != cardinality:
|
||||||
continue
|
continue
|
||||||
if se not in {positions[idx].subevent_id for idx in current_group}:
|
if se not in {positions[idx][1] for idx in current_group}:
|
||||||
candidates += l
|
candidates += l
|
||||||
cardinality = len(l)
|
cardinality = len(l)
|
||||||
|
|
||||||
@@ -440,7 +403,7 @@ class Discount(LoggedModel):
|
|||||||
|
|
||||||
# Sort the list by prices, then pick one. For "buy 2 get 1 free" we apply a "pick 1 from the start
|
# Sort the list by prices, then pick one. For "buy 2 get 1 free" we apply a "pick 1 from the start
|
||||||
# and 2 from the end" scheme to optimize price distribution among groups
|
# and 2 from the end" scheme to optimize price distribution among groups
|
||||||
candidates = sorted(candidates, key=lambda idx: positions[idx].line_price_gross)
|
candidates = sorted(candidates, key=lambda idx: positions[idx][2])
|
||||||
if len(current_group) < (self.benefit_only_apply_to_cheapest_n_matches or 0):
|
if len(current_group) < (self.benefit_only_apply_to_cheapest_n_matches or 0):
|
||||||
candidate = candidates[0]
|
candidate = candidates[0]
|
||||||
else:
|
else:
|
||||||
@@ -452,14 +415,14 @@ class Discount(LoggedModel):
|
|||||||
if len(current_group) >= max(self.condition_min_count, 1):
|
if len(current_group) >= max(self.condition_min_count, 1):
|
||||||
candidate_groups.append(current_group)
|
candidate_groups.append(current_group)
|
||||||
for c in current_group:
|
for c in current_group:
|
||||||
subevent_to_idx[positions[c].subevent_id].remove(c)
|
subevent_to_idx[positions[c][1]].remove(c)
|
||||||
current_group = []
|
current_group = []
|
||||||
|
|
||||||
# Distribute "leftovers"
|
# Distribute "leftovers"
|
||||||
for se in subevent_order:
|
for se in subevent_order:
|
||||||
if subevent_to_idx[se]:
|
if subevent_to_idx[se]:
|
||||||
for group in candidate_groups:
|
for group in candidate_groups:
|
||||||
if se not in {positions[idx].subevent_id for idx in group}:
|
if se not in {positions[idx][1] for idx in group}:
|
||||||
group.append(subevent_to_idx[se].pop())
|
group.append(subevent_to_idx[se].pop())
|
||||||
if not subevent_to_idx[se]:
|
if not subevent_to_idx[se]:
|
||||||
break
|
break
|
||||||
@@ -469,8 +432,6 @@ class Discount(LoggedModel):
|
|||||||
positions,
|
positions,
|
||||||
[idx for idx in g if idx in condition_candidates],
|
[idx for idx in g if idx in condition_candidates],
|
||||||
[idx for idx in g if idx in benefit_candidates],
|
[idx for idx in g if idx in benefit_candidates],
|
||||||
result,
|
result
|
||||||
None,
|
|
||||||
None
|
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -313,38 +313,9 @@ class EventMixin:
|
|||||||
items=GroupConcat('pk', delimiter=',')
|
items=GroupConcat('pk', delimiter=',')
|
||||||
).values('items')
|
).values('items')
|
||||||
|
|
||||||
q_variation = (
|
sq_active_variation = ItemVariation.objects.filter_available(channel=channel, voucher=voucher).filter(
|
||||||
Q(active=True)
|
Q(quotas__pk=OuterRef('pk'))
|
||||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
|
).order_by().values_list('quotas__pk').annotate(
|
||||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
|
|
||||||
& Q(item__active=True)
|
|
||||||
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
|
|
||||||
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
|
|
||||||
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
|
|
||||||
& Q(item__require_bundling=False)
|
|
||||||
& Q(quotas__pk=OuterRef('pk'))
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(channel, str):
|
|
||||||
q_variation &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel))
|
|
||||||
q_variation &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels__identifier=channel))
|
|
||||||
else:
|
|
||||||
q_variation &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels=channel))
|
|
||||||
q_variation &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels=channel))
|
|
||||||
|
|
||||||
if voucher:
|
|
||||||
if voucher.variation_id:
|
|
||||||
q_variation &= Q(pk=voucher.variation_id)
|
|
||||||
elif voucher.item_id:
|
|
||||||
q_variation &= Q(item_id=voucher.item_id)
|
|
||||||
elif voucher.quota_id:
|
|
||||||
q_variation &= Q(quotas__in=[voucher.quota_id])
|
|
||||||
|
|
||||||
if not voucher or not voucher.show_hidden_items:
|
|
||||||
q_variation &= Q(hide_without_voucher=False)
|
|
||||||
q_variation &= Q(item__hide_without_voucher=False)
|
|
||||||
|
|
||||||
sq_active_variation = ItemVariation.objects.filter(q_variation).order_by().values_list('quotas__pk').annotate(
|
|
||||||
items=GroupConcat('pk', delimiter=',')
|
items=GroupConcat('pk', delimiter=',')
|
||||||
).values('items')
|
).values('items')
|
||||||
quota_base_qs = Quota.objects.using(settings.DATABASE_REPLICA).filter(
|
quota_base_qs = Quota.objects.using(settings.DATABASE_REPLICA).filter(
|
||||||
@@ -823,9 +794,6 @@ 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(
|
||||||
@@ -873,12 +841,10 @@ class Event(EventMixin, LoggedModel):
|
|||||||
for i in Item.objects.filter(event=other).prefetch_related(
|
for i in Item.objects.filter(event=other).prefetch_related(
|
||||||
'variations', 'limit_sales_channels', 'require_membership_types',
|
'variations', 'limit_sales_channels', 'require_membership_types',
|
||||||
'variations__limit_sales_channels', 'variations__require_membership_types',
|
'variations__limit_sales_channels', 'variations__require_membership_types',
|
||||||
'matched_by_cross_selling_categories',
|
|
||||||
):
|
):
|
||||||
vars = list(i.variations.all())
|
vars = list(i.variations.all())
|
||||||
require_membership_types = list(i.require_membership_types.all())
|
require_membership_types = list(i.require_membership_types.all())
|
||||||
limit_sales_channels = list(i.limit_sales_channels.all())
|
limit_sales_channels = list(i.limit_sales_channels.all())
|
||||||
matched_by_cross_selling_categories = list(i.matched_by_cross_selling_categories.all())
|
|
||||||
item_map[i.pk] = i
|
item_map[i.pk] = i
|
||||||
i.pk = None
|
i.pk = None
|
||||||
i.event = self
|
i.event = self
|
||||||
@@ -916,9 +882,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
if not v.all_sales_channels:
|
if not v.all_sales_channels:
|
||||||
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||||
|
|
||||||
if matched_by_cross_selling_categories:
|
|
||||||
i.matched_by_cross_selling_categories.set([category_map[c.pk] for c in matched_by_cross_selling_categories])
|
|
||||||
|
|
||||||
for i in self.items.filter(hidden_if_item_available__isnull=False):
|
for i in self.items.filter(hidden_if_item_available__isnull=False):
|
||||||
i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
|
i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
|
||||||
i.save()
|
i.save()
|
||||||
@@ -1027,9 +990,10 @@ 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'
|
'limit_products', 'auto_checkin_sales_channels'
|
||||||
):
|
):
|
||||||
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 = {}
|
||||||
@@ -1041,6 +1005,8 @@ 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,7 +362,6 @@ 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)
|
||||||
|
|||||||
@@ -63,13 +63,14 @@ from django_countries.fields import Country
|
|||||||
from django_scopes import ScopedManager
|
from django_scopes import ScopedManager
|
||||||
from i18nfield.fields import I18nCharField, I18nTextField
|
from i18nfield.fields import I18nCharField, I18nTextField
|
||||||
|
|
||||||
from pretix.base.media import MEDIA_TYPES
|
|
||||||
from pretix.base.models import Event, SubEvent
|
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.models.fields import MultiStringField
|
from pretix.base.models.fields import MultiStringField
|
||||||
from pretix.base.models.tax import TaxedPrice
|
from pretix.base.models.tax import TaxedPrice
|
||||||
from pretix.base.timemachine import time_machine_now
|
from pretix.base.timemachine import time_machine_now
|
||||||
from pretix.helpers.images import ImageSizeValidator
|
|
||||||
|
from ...helpers.images import ImageSizeValidator
|
||||||
|
from ..media import MEDIA_TYPES
|
||||||
|
from .event import Event, SubEvent
|
||||||
|
|
||||||
|
|
||||||
class ItemCategory(LoggedModel):
|
class ItemCategory(LoggedModel):
|
||||||
@@ -110,33 +111,6 @@ class ItemCategory(LoggedModel):
|
|||||||
'only be bought in combination with a product that has this category configured as a possible '
|
'only be bought in combination with a product that has this category configured as a possible '
|
||||||
'source for add-ons.')
|
'source for add-ons.')
|
||||||
)
|
)
|
||||||
CROSS_SELLING_MODES = (
|
|
||||||
(None, _('Normal category')),
|
|
||||||
('both', _('Normal + cross-selling category')),
|
|
||||||
('only', _('Cross-selling category')),
|
|
||||||
)
|
|
||||||
cross_selling_mode = models.CharField(
|
|
||||||
choices=CROSS_SELLING_MODES,
|
|
||||||
null=True,
|
|
||||||
max_length=5
|
|
||||||
)
|
|
||||||
CROSS_SELLING_CONDITION = (
|
|
||||||
('always', _('Always show in cross-selling step')),
|
|
||||||
('discounts', _('Only show products that qualify for a discount according to discount rules')),
|
|
||||||
('products', _('Only show if the cart contains one of the following products')),
|
|
||||||
)
|
|
||||||
cross_selling_condition = models.CharField(
|
|
||||||
verbose_name=_("Cross-selling condition"),
|
|
||||||
choices=CROSS_SELLING_CONDITION,
|
|
||||||
null=True,
|
|
||||||
max_length=10,
|
|
||||||
)
|
|
||||||
cross_selling_match_products = models.ManyToManyField(
|
|
||||||
'pretixbase.Item',
|
|
||||||
blank=True,
|
|
||||||
verbose_name=_("Cross-selling condition products"),
|
|
||||||
related_name="matched_by_cross_selling_categories",
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Product category")
|
verbose_name = _("Product category")
|
||||||
@@ -145,31 +119,19 @@ class ItemCategory(LoggedModel):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
name = self.internal_name or self.name
|
name = self.internal_name or self.name
|
||||||
if self.category_type != 'normal':
|
if self.is_addon:
|
||||||
return _('{category} ({category_type})').format(category=str(name),
|
return _('{category} (Add-On products)').format(category=str(name))
|
||||||
category_type=self.get_category_type_display())
|
|
||||||
return str(name)
|
return str(name)
|
||||||
|
|
||||||
def get_category_type_display(self):
|
def get_category_type_display(self):
|
||||||
if self.is_addon:
|
if self.is_addon:
|
||||||
return _('Add-on category')
|
return _('Add-On products')
|
||||||
elif self.cross_selling_mode:
|
|
||||||
return self.get_cross_selling_mode_display()
|
|
||||||
else:
|
else:
|
||||||
return _('Normal category')
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def category_type(self):
|
def category_type(self):
|
||||||
return 'addon' if self.is_addon else self.cross_selling_mode or 'normal'
|
return 'addon' if self.is_addon else 'normal'
|
||||||
|
|
||||||
@category_type.setter
|
|
||||||
def category_type(self, new_value):
|
|
||||||
if new_value == 'addon':
|
|
||||||
self.is_addon = True
|
|
||||||
self.cross_selling_mode = None
|
|
||||||
else:
|
|
||||||
self.is_addon = False
|
|
||||||
self.cross_selling_mode = None if new_value == 'normal' else new_value
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
@@ -308,7 +270,7 @@ class SubEventItemVariation(models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
|
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
||||||
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
|
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
|
||||||
# makes the query SIGNIFICANTLY faster
|
# makes the query SIGNIFICANTLY faster
|
||||||
from .organizer import SalesChannel
|
from .organizer import SalesChannel
|
||||||
@@ -329,8 +291,6 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_
|
|||||||
|
|
||||||
if not allow_addons:
|
if not allow_addons:
|
||||||
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
||||||
if not allow_cross_sell:
|
|
||||||
q &= Q(Q(category__isnull=True) | ~Q(category__cross_selling_mode='only'))
|
|
||||||
|
|
||||||
if voucher:
|
if voucher:
|
||||||
if voucher.item_id:
|
if voucher.item_id:
|
||||||
@@ -343,9 +303,51 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_
|
|||||||
return qs.filter(q)
|
return qs.filter(q)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_variations_available(qs, channel='web', voucher=None, allow_addons=False):
|
||||||
|
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
|
||||||
|
# makes the query SIGNIFICANTLY faster
|
||||||
|
from .organizer import SalesChannel
|
||||||
|
|
||||||
|
assert isinstance(channel, (SalesChannel, str))
|
||||||
|
q = (
|
||||||
|
Q(active=True)
|
||||||
|
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
|
||||||
|
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
|
||||||
|
& Q(item__active=True)
|
||||||
|
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
|
||||||
|
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
|
||||||
|
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
|
||||||
|
& Q(item__require_bundling=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(channel, str):
|
||||||
|
q &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel))
|
||||||
|
q &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels__identifier=channel))
|
||||||
|
else:
|
||||||
|
q &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels=channel))
|
||||||
|
q &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels=channel))
|
||||||
|
|
||||||
|
if not allow_addons:
|
||||||
|
q &= Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
|
||||||
|
|
||||||
|
if voucher:
|
||||||
|
if voucher.variation_id:
|
||||||
|
q &= Q(pk=voucher.variation_id)
|
||||||
|
elif voucher.item_id:
|
||||||
|
q &= Q(item_id=voucher.item_id)
|
||||||
|
elif voucher.quota_id:
|
||||||
|
q &= Q(quotas__in=[voucher.quota_id])
|
||||||
|
|
||||||
|
if not voucher or not voucher.show_hidden_items:
|
||||||
|
q &= Q(hide_without_voucher=False)
|
||||||
|
q &= Q(item__hide_without_voucher=False)
|
||||||
|
|
||||||
|
return qs.filter(q)
|
||||||
|
|
||||||
|
|
||||||
class ItemQuerySet(models.QuerySet):
|
class ItemQuerySet(models.QuerySet):
|
||||||
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
|
def filter_available(self, channel='web', voucher=None, allow_addons=False):
|
||||||
return filter_available(self, channel, voucher, allow_addons, allow_cross_sell)
|
return filter_available(self, channel, voucher, allow_addons)
|
||||||
|
|
||||||
|
|
||||||
class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__):
|
class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__):
|
||||||
@@ -353,8 +355,22 @@ class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__)
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self._queryset_class = ItemQuerySet
|
self._queryset_class = ItemQuerySet
|
||||||
|
|
||||||
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
|
def filter_available(self, channel='web', voucher=None, allow_addons=False):
|
||||||
return filter_available(self.get_queryset(), channel, voucher, allow_addons, allow_cross_sell)
|
return filter_available(self.get_queryset(), channel, voucher, allow_addons)
|
||||||
|
|
||||||
|
|
||||||
|
class ItemVariationQuerySet(models.QuerySet):
|
||||||
|
def filter_available(self, channel='web', voucher=None, allow_addons=False):
|
||||||
|
return filter_variations_available(self, channel, voucher, allow_addons)
|
||||||
|
|
||||||
|
|
||||||
|
class ItemVariationQuerySetManager(ScopedManager(organizer='item__event__organizer').__class__):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._queryset_class = ItemVariationQuerySet
|
||||||
|
|
||||||
|
def filter_available(self, channel='web', voucher=None, allow_addons=False):
|
||||||
|
return filter_variations_available(self.get_queryset(), channel, voucher, allow_addons)
|
||||||
|
|
||||||
|
|
||||||
class Item(LoggedModel):
|
class Item(LoggedModel):
|
||||||
@@ -442,12 +458,8 @@ class Item(LoggedModel):
|
|||||||
UNAVAIL_MODE_INFO = "info"
|
UNAVAIL_MODE_INFO = "info"
|
||||||
UNAVAIL_MODES = (
|
UNAVAIL_MODES = (
|
||||||
(UNAVAIL_MODE_HIDDEN, _("Hide product if unavailable")),
|
(UNAVAIL_MODE_HIDDEN, _("Hide product if unavailable")),
|
||||||
(UNAVAIL_MODE_INFO, _("Show product with info on why it’s unavailable")),
|
(UNAVAIL_MODE_INFO, _("Show info text if unavailable")),
|
||||||
)
|
)
|
||||||
UNAVAIL_MODE_ICONS = {
|
|
||||||
UNAVAIL_MODE_HIDDEN: 'eye-slash',
|
|
||||||
UNAVAIL_MODE_INFO: 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
MEDIA_POLICY_REUSE = 'reuse'
|
MEDIA_POLICY_REUSE = 'reuse'
|
||||||
MEDIA_POLICY_NEW = 'new'
|
MEDIA_POLICY_NEW = 'new'
|
||||||
@@ -600,11 +612,6 @@ class Item(LoggedModel):
|
|||||||
"be a short period in which both products are visible while all tickets of the referenced "
|
"be a short period in which both products are visible while all tickets of the referenced "
|
||||||
"product are reserved, but not yet sold.")
|
"product are reserved, but not yet sold.")
|
||||||
)
|
)
|
||||||
hidden_if_item_available_mode = models.CharField(
|
|
||||||
choices=UNAVAIL_MODES,
|
|
||||||
default=UNAVAIL_MODE_HIDDEN,
|
|
||||||
max_length=16,
|
|
||||||
)
|
|
||||||
require_voucher = models.BooleanField(
|
require_voucher = models.BooleanField(
|
||||||
verbose_name=_('This product can only be bought using a voucher.'),
|
verbose_name=_('This product can only be bought using a voucher.'),
|
||||||
default=False,
|
default=False,
|
||||||
@@ -846,7 +853,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='', code=None)
|
rate=Decimal('0.00'), name='')
|
||||||
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,
|
||||||
@@ -854,7 +861,6 @@ 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
|
||||||
@@ -894,8 +900,6 @@ class Item(LoggedModel):
|
|||||||
return 'available_from'
|
return 'available_from'
|
||||||
elif subevent_item and subevent_item.available_until and subevent_item.available_until < now_dt:
|
elif subevent_item and subevent_item.available_until and subevent_item.available_until < now_dt:
|
||||||
return 'available_until'
|
return 'available_until'
|
||||||
elif self.hidden_if_item_available and self._dependency_available:
|
|
||||||
return 'hidden_if_item_available'
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1130,12 +1134,13 @@ 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,
|
||||||
@@ -1250,7 +1255,7 @@ class ItemVariation(models.Model):
|
|||||||
help_text=_('This text will be shown by the check-in app if a ticket of this type is scanned.')
|
help_text=_('This text will be shown by the check-in app if a ticket of this type is scanned.')
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='item__event__organizer')
|
objects = ItemVariationQuerySetManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Product variation")
|
verbose_name = _("Product variation")
|
||||||
@@ -1270,7 +1275,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='', code=None)
|
rate=Decimal('0.00'), name='')
|
||||||
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,
|
||||||
@@ -1292,7 +1297,6 @@ 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
|
||||||
|
|
||||||
@@ -1729,10 +1733,10 @@ class Question(LoggedModel):
|
|||||||
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
||||||
)
|
)
|
||||||
dependency_values = MultiStringField(default=[])
|
dependency_values = MultiStringField(default=[])
|
||||||
valid_number_min = models.DecimalField(decimal_places=6, max_digits=30, null=True, blank=True,
|
valid_number_min = models.DecimalField(decimal_places=6, max_digits=16, null=True, blank=True,
|
||||||
verbose_name=_('Minimum value'),
|
verbose_name=_('Minimum value'),
|
||||||
help_text=_('Currently not supported in our apps and during check-in'))
|
help_text=_('Currently not supported in our apps and during check-in'))
|
||||||
valid_number_max = models.DecimalField(decimal_places=6, max_digits=30, null=True, blank=True,
|
valid_number_max = models.DecimalField(decimal_places=6, max_digits=16, null=True, blank=True,
|
||||||
verbose_name=_('Maximum value'),
|
verbose_name=_('Maximum value'),
|
||||||
help_text=_('Currently not supported in our apps and during check-in'))
|
help_text=_('Currently not supported in our apps and during check-in'))
|
||||||
valid_date_min = models.DateField(null=True, blank=True,
|
valid_date_min = models.DateField(null=True, blank=True,
|
||||||
|
|||||||
@@ -33,15 +33,16 @@
|
|||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.html import escape
|
||||||
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
from pretix.base.logentrytype_registry import log_entry_types, make_link
|
from pretix.base.signals import logentry_object_link
|
||||||
from pretix.base.signals import is_app_active, logentry_object_link
|
|
||||||
|
|
||||||
|
|
||||||
class VisibleOnlyManager(models.Manager):
|
class VisibleOnlyManager(models.Manager):
|
||||||
@@ -91,10 +92,6 @@ class LogEntry(models.Model):
|
|||||||
indexes = [models.Index(fields=["datetime", "id"])]
|
indexes = [models.Index(fields=["datetime", "id"])]
|
||||||
|
|
||||||
def display(self):
|
def display(self):
|
||||||
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
|
|
||||||
if log_entry_type:
|
|
||||||
return log_entry_type.display(self, self.parsed_data)
|
|
||||||
|
|
||||||
from ..signals import logentry_display
|
from ..signals import logentry_display
|
||||||
|
|
||||||
for receiver, response in logentry_display.send(self.event, logentry=self):
|
for receiver, response in logentry_display.send(self.event, logentry=self):
|
||||||
@@ -129,18 +126,10 @@ class LogEntry(models.Model):
|
|||||||
@cached_property
|
@cached_property
|
||||||
def display_object(self):
|
def display_object(self):
|
||||||
from . import (
|
from . import (
|
||||||
Discount, Event, Item, Order, Question, Quota, SubEvent, Voucher,
|
Discount, Event, Item, ItemCategory, Order, Question, Quota,
|
||||||
|
SubEvent, TaxRule, Voucher,
|
||||||
)
|
)
|
||||||
|
|
||||||
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
|
|
||||||
if log_entry_type:
|
|
||||||
link_info = log_entry_type.get_object_link_info(self)
|
|
||||||
if is_app_active(self.event, meta['plugin']):
|
|
||||||
return make_link(link_info, log_entry_type.object_link_wrapper)
|
|
||||||
else:
|
|
||||||
return make_link(link_info, log_entry_type.object_link_wrapper, is_active=False,
|
|
||||||
event=self.event, plugin_name=meta['plugin'] and getattr(meta['plugin'], 'name'))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.content_type.model_class() is Event:
|
if self.content_type.model_class() is Event:
|
||||||
return ''
|
return ''
|
||||||
@@ -148,15 +137,110 @@ class LogEntry(models.Model):
|
|||||||
co = self.content_object
|
co = self.content_object
|
||||||
except:
|
except:
|
||||||
return ''
|
return ''
|
||||||
|
a_map = None
|
||||||
|
a_text = None
|
||||||
|
|
||||||
for receiver, response in logentry_object_link.send(self.event, logentry=self):
|
if isinstance(co, Order):
|
||||||
if response:
|
a_text = _('Order {val}')
|
||||||
return response
|
a_map = {
|
||||||
|
'href': reverse('control:event.order', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
'code': co.code
|
||||||
|
}),
|
||||||
|
'val': escape(co.code),
|
||||||
|
}
|
||||||
|
elif isinstance(co, Voucher):
|
||||||
|
a_text = _('Voucher {val}…')
|
||||||
|
a_map = {
|
||||||
|
'href': reverse('control:event.voucher', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
'voucher': co.id
|
||||||
|
}),
|
||||||
|
'val': escape(co.code[:6]),
|
||||||
|
}
|
||||||
|
elif isinstance(co, Item):
|
||||||
|
a_text = _('Product {val}')
|
||||||
|
a_map = {
|
||||||
|
'href': reverse('control:event.item', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
'item': co.id
|
||||||
|
}),
|
||||||
|
'val': escape(co.name),
|
||||||
|
}
|
||||||
|
elif isinstance(co, SubEvent):
|
||||||
|
a_text = pgettext_lazy('subevent', 'Date {val}')
|
||||||
|
a_map = {
|
||||||
|
'href': reverse('control:event.subevent', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
'subevent': co.id
|
||||||
|
}),
|
||||||
|
'val': escape(str(co))
|
||||||
|
}
|
||||||
|
elif isinstance(co, Quota):
|
||||||
|
a_text = _('Quota {val}')
|
||||||
|
a_map = {
|
||||||
|
'href': reverse('control:event.items.quotas.show', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
'quota': co.id
|
||||||
|
}),
|
||||||
|
'val': escape(co.name),
|
||||||
|
}
|
||||||
|
elif isinstance(co, Discount):
|
||||||
|
a_text = _('Discount {val}')
|
||||||
|
a_map = {
|
||||||
|
'href': reverse('control:event.items.discounts.edit', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
'discount': co.id
|
||||||
|
}),
|
||||||
|
'val': escape(co.internal_name),
|
||||||
|
}
|
||||||
|
elif isinstance(co, ItemCategory):
|
||||||
|
a_text = _('Category {val}')
|
||||||
|
a_map = {
|
||||||
|
'href': reverse('control:event.items.categories.edit', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
'category': co.id
|
||||||
|
}),
|
||||||
|
'val': escape(co.name),
|
||||||
|
}
|
||||||
|
elif isinstance(co, Question):
|
||||||
|
a_text = _('Question {val}')
|
||||||
|
a_map = {
|
||||||
|
'href': reverse('control:event.items.questions.show', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
'question': co.id
|
||||||
|
}),
|
||||||
|
'val': escape(co.question),
|
||||||
|
}
|
||||||
|
elif isinstance(co, TaxRule):
|
||||||
|
a_text = _('Tax rule {val}')
|
||||||
|
a_map = {
|
||||||
|
'href': reverse('control:event.settings.tax.edit', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
'rule': co.id
|
||||||
|
}),
|
||||||
|
'val': escape(co.name),
|
||||||
|
}
|
||||||
|
|
||||||
if isinstance(co, (Order, Voucher, Item, SubEvent, Quota, Discount, Question)):
|
if a_text and a_map:
|
||||||
logging.warning("LogEntryType missing or ill-defined: %s", self.action_type)
|
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
|
||||||
|
return a_text.format_map(a_map)
|
||||||
return ''
|
elif a_text:
|
||||||
|
return a_text
|
||||||
|
else:
|
||||||
|
for receiver, response in logentry_object_link.send(self.event, logentry=self):
|
||||||
|
if response:
|
||||||
|
return response
|
||||||
|
return ''
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def parsed_data(self):
|
def parsed_data(self):
|
||||||
|
|||||||
@@ -159,24 +159,10 @@ 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', _('Email')),
|
('mail', _('E-mail')),
|
||||||
)
|
)
|
||||||
user = models.ForeignKey('User', on_delete=models.CASCADE,
|
user = models.ForeignKey('User', on_delete=models.CASCADE,
|
||||||
related_name='notification_settings')
|
related_name='notification_settings')
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
import string
|
import string
|
||||||
import warnings
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@@ -62,10 +61,9 @@ 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, force_str
|
from django.utils.encoding import escape_uri_path
|
||||||
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
|
||||||
@@ -243,7 +241,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
)
|
)
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
verbose_name=_('Email')
|
verbose_name=_('E-mail')
|
||||||
)
|
)
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
@@ -318,7 +316,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
)
|
)
|
||||||
email_known_to_work = models.BooleanField(
|
email_known_to_work = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name=_('Email address verified')
|
verbose_name=_('E-mail 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
|
||||||
@@ -355,7 +353,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
|
|
||||||
if not self.testmode:
|
if not self.testmode:
|
||||||
raise TypeError("Only test mode orders can be deleted.")
|
raise TypeError("Only test mode orders can be deleted.")
|
||||||
self.log_action(
|
self.event.log_action(
|
||||||
'pretix.event.order.deleted', user=user, auth=auth,
|
'pretix.event.order.deleted', user=user, auth=auth,
|
||||||
data={
|
data={
|
||||||
'code': self.code,
|
'code': self.code,
|
||||||
@@ -383,28 +381,8 @@ class Order(LockModel, LoggedModel):
|
|||||||
self.event.cache.delete('complain_testmode_orders')
|
self.event.cache.delete('complain_testmode_orders')
|
||||||
self.delete()
|
self.delete()
|
||||||
|
|
||||||
def email_confirm_secret(self):
|
|
||||||
return self.tagged_secret("email_confirm", 9)
|
|
||||||
|
|
||||||
def email_confirm_hash(self):
|
def email_confirm_hash(self):
|
||||||
warnings.warn('Use email_confirm_secret() instead of email_confirm_hash().',
|
return hashlib.sha256(settings.SECRET_KEY.encode() + self.secret.encode()).hexdigest()[:9]
|
||||||
DeprecationWarning)
|
|
||||||
return self.email_confirm_secret()
|
|
||||||
|
|
||||||
def check_email_confirm_secret(self, received_secret):
|
|
||||||
return (
|
|
||||||
hmac.compare_digest(
|
|
||||||
self.tagged_secret("email_confirm", 9),
|
|
||||||
received_secret[:9].lower()
|
|
||||||
) or any(
|
|
||||||
# TODO: remove this clause after a while (compatibility with old secrets currently in flight)
|
|
||||||
hmac.compare_digest(
|
|
||||||
hashlib.sha256(sk.encode() + self.secret.encode()).hexdigest()[:9],
|
|
||||||
received_secret
|
|
||||||
)
|
|
||||||
for sk in [settings.SECRET_KEY, *settings.SECRET_KEY_FALLBACKS]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_extended_status_display(self):
|
def get_extended_status_display(self):
|
||||||
# Changes in this method should to be replicated in pretixcontrol/orders/fragment_order_status.html
|
# Changes in this method should to be replicated in pretixcontrol/orders/fragment_order_status.html
|
||||||
@@ -1087,7 +1065,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
|
|
||||||
for i, op in enumerate(positions):
|
for i, op in enumerate(positions):
|
||||||
if op.seat:
|
if op.seat:
|
||||||
if not op.seat.is_available(ignore_orderpos=op, sales_channel=self.sales_channel.identifier):
|
if not op.seat.is_available(ignore_orderpos=op):
|
||||||
raise Quota.QuotaExceededException(error_messages['seat_unavailable'].format(seat=op.seat))
|
raise Quota.QuotaExceededException(error_messages['seat_unavailable'].format(seat=op.seat))
|
||||||
if force:
|
if force:
|
||||||
continue
|
continue
|
||||||
@@ -1257,7 +1235,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, taxcode = k
|
positionid, itemid, variationid, subeventid, price, taxrate, taxruleid, taxvalue, feetype, internaltype = 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(
|
||||||
@@ -1273,7 +1251,6 @@ 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,
|
||||||
))
|
))
|
||||||
@@ -2277,16 +2254,14 @@ 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 = (
|
||||||
(FEE_TYPE_SERVICE, _("Service fee")),
|
|
||||||
(FEE_TYPE_PAYMENT, _("Payment fee")),
|
(FEE_TYPE_PAYMENT, _("Payment fee")),
|
||||||
(FEE_TYPE_SHIPPING, _("Shipping fee")),
|
(FEE_TYPE_SHIPPING, _("Shipping 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")),
|
||||||
)
|
)
|
||||||
@@ -2315,10 +2290,6 @@ 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')
|
||||||
@@ -2346,16 +2317,6 @@ 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
|
||||||
@@ -2386,11 +2347,9 @@ 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):
|
||||||
@@ -2399,7 +2358,6 @@ 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():
|
||||||
@@ -2487,10 +2445,6 @@ 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')
|
||||||
@@ -2548,16 +2502,6 @@ 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
|
||||||
@@ -2730,13 +2674,11 @@ 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):
|
||||||
@@ -3007,10 +2949,6 @@ 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')
|
||||||
@@ -3031,27 +2969,17 @@ 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_code)
|
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type)
|
||||||
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_code)
|
obj.tax_rule_id, obj.tax_value, None, None)
|
||||||
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_code)
|
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type)
|
||||||
raise ValueError('invalid state') # noqa
|
raise ValueError('invalid state') # noqa
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -3215,15 +3143,8 @@ 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
|
|
||||||
def discount_percentage(self):
|
|
||||||
if not self.line_price_gross:
|
|
||||||
return 0
|
|
||||||
return (self.line_price_gross - self.price) / self.line_price_gross * 100
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def addons_without_bundled(self):
|
def addons_without_bundled(self):
|
||||||
addons = [op for op in self.addons.all() if not op.is_bundled]
|
addons = [op for op in self.addons.all() if not op.is_bundled]
|
||||||
@@ -3262,9 +3183,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=True)
|
street = models.TextField(verbose_name=_('Address'), blank=False)
|
||||||
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True)
|
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
|
||||||
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True)
|
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
|
||||||
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)
|
||||||
@@ -3449,74 +3370,6 @@ 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,30 +53,6 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
#
|
#
|
||||||
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
|
||||||
@@ -31,9 +30,8 @@ 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, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext
|
||||||
from i18nfield.fields import I18nCharField
|
from i18nfield.fields import I18nCharField
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
@@ -44,7 +42,7 @@ from pretix.helpers.countries import FastCountryField
|
|||||||
|
|
||||||
|
|
||||||
class TaxedPrice:
|
class TaxedPrice:
|
||||||
def __init__(self, *, gross: Decimal, net: Decimal, tax: Decimal, rate: Decimal, name: str, code: Optional[str]):
|
def __init__(self, *, gross: Decimal, net: Decimal, tax: Decimal, rate: Decimal, name: 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
|
||||||
@@ -52,7 +50,6 @@ 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))
|
||||||
@@ -75,7 +72,6 @@ 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):
|
||||||
@@ -89,7 +85,6 @@ 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):
|
||||||
@@ -98,8 +93,7 @@ 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 and
|
self.name == other.name
|
||||||
self.code == other.code
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -108,8 +102,7 @@ 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 = {
|
||||||
@@ -132,152 +125,6 @@ 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
|
||||||
@@ -326,14 +173,6 @@ 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,
|
||||||
@@ -411,16 +250,6 @@ 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)
|
||||||
@@ -447,9 +276,8 @@ 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, override_tax_code=None,
|
def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, invoice_address=None,
|
||||||
invoice_address=None, subtract_from_gross=Decimal('0.00'), gross_price_is_tax_rate: Decimal = None,
|
subtract_from_gross=Decimal('0.00'), gross_price_is_tax_rate: Decimal = None, force_fixed_gross_price=False):
|
||||||
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
|
||||||
@@ -457,13 +285,6 @@ 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:
|
||||||
@@ -483,21 +304,10 @@ class TaxRule(LoggedModel):
|
|||||||
subtract_from_gross = Decimal('0.00')
|
subtract_from_gross = Decimal('0.00')
|
||||||
rate = adjust_rate
|
rate = adjust_rate
|
||||||
|
|
||||||
def _limit_subtract(base_price, subtract_from_gross):
|
|
||||||
if not subtract_from_gross:
|
|
||||||
return base_price
|
|
||||||
if base_price >= Decimal('0.00'):
|
|
||||||
# For positive prices, make sure they don't go negative because of bundles
|
|
||||||
return max(Decimal('0.00'), base_price - subtract_from_gross)
|
|
||||||
else:
|
|
||||||
# If the price is already negative, we don't really care any more
|
|
||||||
return base_price - subtract_from_gross
|
|
||||||
|
|
||||||
if rate == Decimal('0.00'):
|
if rate == Decimal('0.00'):
|
||||||
gross = _limit_subtract(base_price, subtract_from_gross)
|
|
||||||
return TaxedPrice(
|
return TaxedPrice(
|
||||||
net=gross, gross=gross, tax=Decimal('0.00'),
|
net=base_price - subtract_from_gross, gross=base_price - subtract_from_gross, tax=Decimal('0.00'),
|
||||||
rate=rate, name=self.name, code=code,
|
rate=rate, name=self.name
|
||||||
)
|
)
|
||||||
|
|
||||||
if base_price_is == 'auto':
|
if base_price_is == 'auto':
|
||||||
@@ -507,14 +317,19 @@ class TaxRule(LoggedModel):
|
|||||||
base_price_is = 'net'
|
base_price_is = 'net'
|
||||||
|
|
||||||
if base_price_is == 'gross':
|
if base_price_is == 'gross':
|
||||||
gross = _limit_subtract(base_price, subtract_from_gross)
|
if base_price >= Decimal('0.00'):
|
||||||
|
# For positive prices, make sure they don't go negative because of bundles
|
||||||
|
gross = max(Decimal('0.00'), base_price - subtract_from_gross)
|
||||||
|
else:
|
||||||
|
# If the price is already negative, we don't really care any more
|
||||||
|
gross = base_price - subtract_from_gross
|
||||||
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
|
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
|
||||||
currency)
|
currency)
|
||||||
elif base_price_is == 'net':
|
elif base_price_is == 'net':
|
||||||
net = base_price
|
net = base_price
|
||||||
gross = round_decimal((net * (1 + rate / 100)), currency)
|
gross = round_decimal((net * (1 + rate / 100)), currency)
|
||||||
if subtract_from_gross:
|
if subtract_from_gross:
|
||||||
gross = _limit_subtract(gross, subtract_from_gross)
|
gross -= subtract_from_gross
|
||||||
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
|
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
|
||||||
currency)
|
currency)
|
||||||
else:
|
else:
|
||||||
@@ -522,7 +337,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, code=code,
|
rate=rate, name=self.name
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -603,38 +418,6 @@ 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=_("Email address")
|
verbose_name=_("E-mail address")
|
||||||
)
|
)
|
||||||
phone = PhoneNumberField(
|
phone = PhoneNumberField(
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
|
|||||||
@@ -1419,51 +1419,50 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _add_giftcard_to_cart(self, cs, gc):
|
|
||||||
from pretix.base.services.cart import add_payment_to_cart_session
|
|
||||||
|
|
||||||
if gc.currency != self.event.currency:
|
|
||||||
raise ValidationError(_("This gift card does not support this currency."))
|
|
||||||
if gc.testmode and not self.event.testmode:
|
|
||||||
raise ValidationError(_("This gift card can only be used in test mode."))
|
|
||||||
if not gc.testmode and self.event.testmode:
|
|
||||||
raise ValidationError(_("Only test gift cards can be used in test mode."))
|
|
||||||
if gc.expires and gc.expires < time_machine_now():
|
|
||||||
raise ValidationError(_("This gift card is no longer valid."))
|
|
||||||
if gc.value <= Decimal("0.00"):
|
|
||||||
raise ValidationError(_("All credit on this gift card has been used."))
|
|
||||||
|
|
||||||
for p in cs.get('payments', []):
|
|
||||||
if p['provider'] == self.identifier and p['info_data']['gift_card'] == gc.pk:
|
|
||||||
raise ValidationError(_("This gift card is already used for your payment."))
|
|
||||||
|
|
||||||
add_payment_to_cart_session(
|
|
||||||
cs,
|
|
||||||
self,
|
|
||||||
max_value=gc.value,
|
|
||||||
info_data={
|
|
||||||
'gift_card': gc.pk,
|
|
||||||
'gift_card_secret': gc.secret,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
|
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
|
||||||
|
from pretix.base.services.cart import add_payment_to_cart
|
||||||
|
|
||||||
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()
|
||||||
)
|
)
|
||||||
cs = cart_session(request)
|
if gc.currency != self.event.currency:
|
||||||
try:
|
messages.error(request, _("This gift card does not support this currency."))
|
||||||
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 "
|
||||||
|
|||||||
@@ -185,104 +185,43 @@ BEFORE_AFTER_CHOICE = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
reldatetimeparts = namedtuple('reldatetimeparts', (
|
|
||||||
"status", # 0
|
|
||||||
"absolute", # 1
|
|
||||||
"rel_days_number", # 2
|
|
||||||
"rel_mins_relationto", # 3
|
|
||||||
"rel_days_timeofday", # 4
|
|
||||||
"rel_mins_number", # 5
|
|
||||||
"rel_days_relationto", # 6
|
|
||||||
"rel_mins_relation", # 7
|
|
||||||
"rel_days_relation" # 8
|
|
||||||
))
|
|
||||||
reldatetimeparts.indizes = reldatetimeparts(*range(9))
|
|
||||||
|
|
||||||
|
|
||||||
class RelativeDateTimeWidget(forms.MultiWidget):
|
class RelativeDateTimeWidget(forms.MultiWidget):
|
||||||
template_name = 'pretixbase/forms/widgets/reldatetime.html'
|
template_name = 'pretixbase/forms/widgets/reldatetime.html'
|
||||||
parts = reldatetimeparts
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.status_choices = kwargs.pop('status_choices')
|
self.status_choices = kwargs.pop('status_choices')
|
||||||
base_choices = kwargs.pop('base_choices')
|
base_choices = kwargs.pop('base_choices')
|
||||||
widgets = reldatetimeparts(
|
widgets = (
|
||||||
status=forms.RadioSelect(choices=self.status_choices),
|
forms.RadioSelect(choices=self.status_choices),
|
||||||
absolute=forms.DateTimeInput(
|
forms.DateTimeInput(
|
||||||
attrs={'class': 'datetimepicker'}
|
attrs={'class': 'datetimepicker'}
|
||||||
),
|
),
|
||||||
rel_days_number=forms.NumberInput(),
|
forms.NumberInput(),
|
||||||
rel_mins_relationto=forms.Select(choices=base_choices),
|
forms.Select(choices=base_choices),
|
||||||
rel_days_timeofday=forms.TimeInput(attrs={'placeholder': _('Time'), 'class': 'timepickerfield'}),
|
forms.TimeInput(attrs={'placeholder': _('Time'), 'class': 'timepickerfield'}),
|
||||||
rel_mins_number=forms.NumberInput(),
|
forms.NumberInput(),
|
||||||
rel_days_relationto=forms.Select(choices=base_choices),
|
forms.Select(choices=base_choices),
|
||||||
rel_mins_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
|
forms.Select(choices=BEFORE_AFTER_CHOICE),
|
||||||
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
|
forms.Select(choices=BEFORE_AFTER_CHOICE),
|
||||||
)
|
)
|
||||||
super().__init__(widgets=widgets, *args, **kwargs)
|
super().__init__(widgets=widgets, *args, **kwargs)
|
||||||
|
|
||||||
def decompress(self, value):
|
def decompress(self, value):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = RelativeDateWrapper.from_string(value)
|
value = RelativeDateWrapper.from_string(value)
|
||||||
if isinstance(value, reldatetimeparts):
|
|
||||||
return value
|
|
||||||
if not value:
|
if not value:
|
||||||
return reldatetimeparts(
|
return ['unset', None, 1, 'date_from', None, 0, "date_from", "before", "before"]
|
||||||
status="unset",
|
|
||||||
absolute=None,
|
|
||||||
rel_days_number=1,
|
|
||||||
rel_mins_relationto="date_from",
|
|
||||||
rel_days_timeofday=None,
|
|
||||||
rel_mins_number=0,
|
|
||||||
rel_days_relationto="date_from",
|
|
||||||
rel_mins_relation="before",
|
|
||||||
rel_days_relation="before"
|
|
||||||
)
|
|
||||||
elif isinstance(value.data, (datetime.datetime, datetime.date)):
|
elif isinstance(value.data, (datetime.datetime, datetime.date)):
|
||||||
return reldatetimeparts(
|
return ['absolute', value.data, 1, 'date_from', None, 0, "date_from", "before", "before"]
|
||||||
status="absolute",
|
|
||||||
absolute=value.data,
|
|
||||||
rel_days_number=1,
|
|
||||||
rel_mins_relationto="date_from",
|
|
||||||
rel_days_timeofday=None,
|
|
||||||
rel_mins_number=0,
|
|
||||||
rel_days_relationto="date_from",
|
|
||||||
rel_mins_relation="before",
|
|
||||||
rel_days_relation="before"
|
|
||||||
)
|
|
||||||
elif value.data.minutes is not None:
|
elif value.data.minutes is not None:
|
||||||
return reldatetimeparts(
|
return ['relative_minutes', None, None, value.data.base_date_name, None, value.data.minutes, value.data.base_date_name,
|
||||||
status="relative_minutes",
|
"after" if value.data.is_after else "before", "after" if value.data.is_after else "before"]
|
||||||
absolute=None,
|
return ['relative', None, value.data.days, value.data.base_date_name, value.data.time, 0, value.data.base_date_name,
|
||||||
rel_days_number=None,
|
"after" if value.data.is_after else "before", "after" if value.data.is_after else "before"]
|
||||||
rel_mins_relationto=value.data.base_date_name,
|
|
||||||
rel_days_timeofday=None,
|
|
||||||
rel_mins_number=value.data.minutes,
|
|
||||||
rel_days_relationto=value.data.base_date_name,
|
|
||||||
rel_mins_relation="after" if value.data.is_after else "before",
|
|
||||||
rel_days_relation="after" if value.data.is_after else "before"
|
|
||||||
)
|
|
||||||
return reldatetimeparts(
|
|
||||||
status="relative",
|
|
||||||
absolute=None,
|
|
||||||
rel_days_number=value.data.days,
|
|
||||||
rel_mins_relationto=value.data.base_date_name,
|
|
||||||
rel_days_timeofday=value.data.time,
|
|
||||||
rel_mins_number=0,
|
|
||||||
rel_days_relationto=value.data.base_date_name,
|
|
||||||
rel_mins_relation="after" if value.data.is_after else "before",
|
|
||||||
rel_days_relation="after" if value.data.is_after else "before"
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_context(self, name, value, attrs):
|
def get_context(self, name, value, attrs):
|
||||||
ctx = super().get_context(name, value, attrs)
|
ctx = super().get_context(name, value, attrs)
|
||||||
ctx['required'] = self.status_choices[0][0] == 'unset'
|
ctx['required'] = self.status_choices[0][0] == 'unset'
|
||||||
|
|
||||||
ctx['rendered_subwidgets'] = self.parts(*(
|
|
||||||
self._render(w['template_name'], {**ctx, 'widget': w})
|
|
||||||
for w in ctx['widget']['subwidgets']
|
|
||||||
))._asdict()
|
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
@@ -300,36 +239,36 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
choices = BASE_CHOICES
|
choices = BASE_CHOICES
|
||||||
if not kwargs.get('required', True):
|
if not kwargs.get('required', True):
|
||||||
status_choices.insert(0, ('unset', _('Not set')))
|
status_choices.insert(0, ('unset', _('Not set')))
|
||||||
fields = reldatetimeparts(
|
fields = (
|
||||||
status=forms.ChoiceField(
|
forms.ChoiceField(
|
||||||
choices=status_choices,
|
choices=status_choices,
|
||||||
required=True
|
required=True
|
||||||
),
|
),
|
||||||
absolute=forms.DateTimeField(
|
forms.DateTimeField(
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
rel_days_number=forms.IntegerField(
|
forms.IntegerField(
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
rel_mins_relationto=forms.ChoiceField(
|
forms.ChoiceField(
|
||||||
choices=choices,
|
choices=choices,
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
rel_days_timeofday=forms.TimeField(
|
forms.TimeField(
|
||||||
required=False,
|
required=False,
|
||||||
),
|
),
|
||||||
rel_mins_number=forms.IntegerField(
|
forms.IntegerField(
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
rel_days_relationto=forms.ChoiceField(
|
forms.ChoiceField(
|
||||||
choices=choices,
|
choices=choices,
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
rel_mins_relation=forms.ChoiceField(
|
forms.ChoiceField(
|
||||||
choices=BEFORE_AFTER_CHOICE,
|
choices=BEFORE_AFTER_CHOICE,
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
rel_days_relation=forms.ChoiceField(
|
forms.ChoiceField(
|
||||||
choices=BEFORE_AFTER_CHOICE,
|
choices=BEFORE_AFTER_CHOICE,
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
@@ -343,36 +282,32 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def set_event(self, event):
|
def set_event(self, event):
|
||||||
self.widget.widgets[reldatetimeparts.indizes.rel_days_relationto].choices = [
|
self.widget.widgets[3].choices = [
|
||||||
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
|
||||||
]
|
|
||||||
self.widget.widgets[reldatetimeparts.indizes.rel_mins_relationto].choices = [
|
|
||||||
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
||||||
]
|
]
|
||||||
|
|
||||||
def compress(self, data_list):
|
def compress(self, data_list):
|
||||||
if not data_list:
|
if not data_list:
|
||||||
return None
|
return None
|
||||||
data = reldatetimeparts(*data_list)
|
if data_list[0] == 'absolute':
|
||||||
if data.status == 'absolute':
|
return RelativeDateWrapper(data_list[1])
|
||||||
return RelativeDateWrapper(data.absolute)
|
elif data_list[0] == 'unset':
|
||||||
elif data.status == 'unset':
|
|
||||||
return None
|
return None
|
||||||
elif data.status == 'relative_minutes':
|
elif data_list[0] == 'relative_minutes':
|
||||||
return RelativeDateWrapper(RelativeDate(
|
return RelativeDateWrapper(RelativeDate(
|
||||||
days=0,
|
days=0,
|
||||||
base_date_name=data.rel_mins_relationto,
|
base_date_name=data_list[3],
|
||||||
time=None,
|
time=None,
|
||||||
minutes=data.rel_mins_number,
|
minutes=data_list[5],
|
||||||
is_after=data.rel_mins_relation == "after",
|
is_after=data_list[7] == "after",
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
return RelativeDateWrapper(RelativeDate(
|
return RelativeDateWrapper(RelativeDate(
|
||||||
days=data.rel_days_number,
|
days=data_list[2],
|
||||||
base_date_name=data.rel_days_relationto,
|
base_date_name=data_list[6],
|
||||||
time=data.rel_days_timeofday,
|
time=data_list[4],
|
||||||
minutes=None,
|
minutes=None,
|
||||||
is_after=data.rel_days_relation == "after",
|
is_after=data_list[8] == "after",
|
||||||
))
|
))
|
||||||
|
|
||||||
def has_changed(self, initial, data):
|
def has_changed(self, initial, data):
|
||||||
@@ -381,41 +316,29 @@ class RelativeDateTimeField(forms.MultiValueField):
|
|||||||
return super().has_changed(initial, data)
|
return super().has_changed(initial, data)
|
||||||
|
|
||||||
def clean(self, value):
|
def clean(self, value):
|
||||||
data = reldatetimeparts(*value)
|
if value[0] == 'absolute' and not value[1]:
|
||||||
if data.status == 'absolute' and not data.absolute:
|
|
||||||
raise ValidationError(self.error_messages['incomplete'])
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
elif data.status == 'relative' and (data.rel_days_number is None or not data.rel_days_relationto):
|
elif value[0] == 'relative' and (value[2] is None or not value[3]):
|
||||||
raise ValidationError(self.error_messages['incomplete'])
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
elif data.status == 'relative_minutes' and (data.rel_mins_number is None or not data.rel_mins_relationto):
|
elif value[0] == 'relative_minutes' and (value[5] is None or not value[3]):
|
||||||
raise ValidationError(self.error_messages['incomplete'])
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
|
|
||||||
return super().clean(value)
|
return super().clean(value)
|
||||||
|
|
||||||
|
|
||||||
reldateparts = namedtuple('reldateparts', (
|
|
||||||
"status", # 0
|
|
||||||
"absolute", # 1
|
|
||||||
"rel_days_number", # 2
|
|
||||||
"rel_days_relationto", # 3
|
|
||||||
"rel_days_relation", # 4
|
|
||||||
))
|
|
||||||
reldateparts.indizes = reldateparts(*range(5))
|
|
||||||
|
|
||||||
|
|
||||||
class RelativeDateWidget(RelativeDateTimeWidget):
|
class RelativeDateWidget(RelativeDateTimeWidget):
|
||||||
template_name = 'pretixbase/forms/widgets/reldate.html'
|
template_name = 'pretixbase/forms/widgets/reldate.html'
|
||||||
parts = reldateparts
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.status_choices = kwargs.pop('status_choices')
|
self.status_choices = kwargs.pop('status_choices')
|
||||||
widgets = reldateparts(
|
widgets = (
|
||||||
status=forms.RadioSelect(choices=self.status_choices),
|
forms.RadioSelect(choices=self.status_choices),
|
||||||
absolute=forms.DateInput(
|
forms.DateInput(
|
||||||
attrs={'class': 'datepickerfield'}
|
attrs={'class': 'datepickerfield'}
|
||||||
),
|
),
|
||||||
rel_days_number=forms.NumberInput(),
|
forms.NumberInput(),
|
||||||
rel_days_relationto=forms.Select(choices=kwargs.pop('base_choices')),
|
forms.Select(choices=kwargs.pop('base_choices')),
|
||||||
rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
|
forms.Select(choices=BEFORE_AFTER_CHOICE),
|
||||||
)
|
)
|
||||||
forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs)
|
forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs)
|
||||||
|
|
||||||
@@ -423,30 +346,10 @@ class RelativeDateWidget(RelativeDateTimeWidget):
|
|||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = RelativeDateWrapper.from_string(value)
|
value = RelativeDateWrapper.from_string(value)
|
||||||
if not value:
|
if not value:
|
||||||
return reldateparts(
|
return ['unset', None, 1, 'date_from', 'before']
|
||||||
status="unset",
|
|
||||||
absolute=None,
|
|
||||||
rel_days_number=1,
|
|
||||||
rel_days_relationto="date_from",
|
|
||||||
rel_days_relation="before"
|
|
||||||
)
|
|
||||||
if isinstance(value, reldateparts):
|
|
||||||
return value
|
|
||||||
elif isinstance(value.data, (datetime.datetime, datetime.date)):
|
elif isinstance(value.data, (datetime.datetime, datetime.date)):
|
||||||
return reldateparts(
|
return ['absolute', value.data, 1, 'date_from', 'before']
|
||||||
status="absolute",
|
return ['relative', None, value.data.days, value.data.base_date_name, "after" if value.data.is_after else "before"]
|
||||||
absolute=value.data,
|
|
||||||
rel_days_number=1,
|
|
||||||
rel_days_relationto="date_from",
|
|
||||||
rel_days_relation="before"
|
|
||||||
)
|
|
||||||
return reldateparts(
|
|
||||||
status="relative",
|
|
||||||
absolute=None,
|
|
||||||
rel_days_number=value.data.days,
|
|
||||||
rel_days_relationto=value.data.base_date_name,
|
|
||||||
rel_days_relation="after" if value.data.is_after else "before"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RelativeDateField(RelativeDateTimeField):
|
class RelativeDateField(RelativeDateTimeField):
|
||||||
@@ -458,22 +361,22 @@ class RelativeDateField(RelativeDateTimeField):
|
|||||||
]
|
]
|
||||||
if not kwargs.get('required', True):
|
if not kwargs.get('required', True):
|
||||||
status_choices.insert(0, ('unset', _('Not set')))
|
status_choices.insert(0, ('unset', _('Not set')))
|
||||||
fields = reldateparts(
|
fields = (
|
||||||
status=forms.ChoiceField(
|
forms.ChoiceField(
|
||||||
choices=status_choices,
|
choices=status_choices,
|
||||||
required=True
|
required=True
|
||||||
),
|
),
|
||||||
absolute=forms.DateField(
|
forms.DateField(
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
rel_days_number=forms.IntegerField(
|
forms.IntegerField(
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
rel_days_relationto=forms.ChoiceField(
|
forms.ChoiceField(
|
||||||
choices=BASE_CHOICES,
|
choices=BASE_CHOICES,
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
rel_days_relation=forms.ChoiceField(
|
forms.ChoiceField(
|
||||||
choices=BEFORE_AFTER_CHOICE,
|
choices=BEFORE_AFTER_CHOICE,
|
||||||
required=False
|
required=False
|
||||||
),
|
),
|
||||||
@@ -484,35 +387,28 @@ class RelativeDateField(RelativeDateTimeField):
|
|||||||
self, fields=fields, require_all_fields=False, *args, **kwargs
|
self, fields=fields, require_all_fields=False, *args, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_event(self, event):
|
|
||||||
self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = [
|
|
||||||
(k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
|
|
||||||
]
|
|
||||||
|
|
||||||
def compress(self, data_list):
|
def compress(self, data_list):
|
||||||
if not data_list:
|
if not data_list:
|
||||||
return None
|
return None
|
||||||
data = reldateparts(*data_list)
|
if data_list[0] == 'absolute':
|
||||||
if data.status == 'absolute':
|
return RelativeDateWrapper(data_list[1])
|
||||||
return RelativeDateWrapper(data.absolute)
|
elif data_list[0] == 'unset':
|
||||||
elif data.status == 'unset':
|
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return RelativeDateWrapper(RelativeDate(
|
return RelativeDateWrapper(RelativeDate(
|
||||||
days=data.rel_days_number,
|
days=data_list[2],
|
||||||
base_date_name=data.rel_days_relationto,
|
base_date_name=data_list[3],
|
||||||
time=None, minutes=None,
|
time=None, minutes=None,
|
||||||
is_after=data.rel_days_relation == "after"
|
is_after=data_list[4] == "after"
|
||||||
))
|
))
|
||||||
|
|
||||||
def clean(self, value):
|
def clean(self, value):
|
||||||
data = reldateparts(*value)
|
if value[0] == 'absolute' and not value[1]:
|
||||||
if data.status == 'absolute' and not data.absolute:
|
|
||||||
raise ValidationError(self.error_messages['incomplete'])
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
elif data.status == 'relative' and (data.rel_days_number is None or not data.rel_days_relationto):
|
elif value[0] == 'relative' and (value[2] is None or not value[3]):
|
||||||
raise ValidationError(self.error_messages['incomplete'])
|
raise ValidationError(self.error_messages['incomplete'])
|
||||||
|
|
||||||
return forms.MultiValueField.clean(self, value)
|
return super().clean(value)
|
||||||
|
|
||||||
|
|
||||||
class ModelRelativeDateTimeField(models.CharField):
|
class ModelRelativeDateTimeField(models.CharField):
|
||||||
|
|||||||
@@ -343,13 +343,11 @@ 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)
|
||||||
@@ -362,7 +360,6 @@ 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]):
|
||||||
@@ -1426,28 +1423,6 @@ 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.
|
||||||
@@ -1462,7 +1437,16 @@ 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)
|
||||||
add_payment_to_cart_session(cs, provider, min_value, max_value, info_data)
|
cs.setdefault('payments', [])
|
||||||
|
|
||||||
|
cs['payments'].append({
|
||||||
|
'id': str(uuid.uuid4()),
|
||||||
|
'provider': provider.identifier,
|
||||||
|
'multi_use_supported': provider.multi_use_supported,
|
||||||
|
'min_value': str(min_value) if min_value is not None else None,
|
||||||
|
'max_value': str(max_value) if max_value is not None else None,
|
||||||
|
'info_data': info_data or {},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def get_fees(event, request, total, invoice_address, payments, positions):
|
def get_fees(event, request, total, invoice_address, payments, positions):
|
||||||
@@ -1513,7 +1497,6 @@ 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
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -1559,9 +1542,10 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
|||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||||
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||||
"""
|
"""
|
||||||
|
Removes a list of items from a user's cart.
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
:param voucher: A voucher code
|
:param voucher: A voucher code
|
||||||
:param cart_id: The cart ID of the cart to modify
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
try:
|
try:
|
||||||
@@ -1582,10 +1566,10 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
|
|||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||||
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||||
"""
|
"""
|
||||||
Removes an item specified by its position ID from a user's cart.
|
Removes a list of items from a user's cart.
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
:param position: A cart position ID
|
:param position: A cart position ID
|
||||||
:param cart_id: The cart ID of the cart to modify
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
try:
|
try:
|
||||||
@@ -1606,9 +1590,9 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
|
|||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||||
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||||
"""
|
"""
|
||||||
Removes all items from a user's cart.
|
Removes a list of items from a user's cart.
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
:param cart_id: The cart ID of the cart to modify
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
try:
|
try:
|
||||||
@@ -1627,15 +1611,13 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
|
|||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||||
def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: List[dict], cart_id: str=None, locale='en',
|
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
|
||||||
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
|
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||||
"""
|
"""
|
||||||
Assigns addons to eligible products in a user's cart, adding and removing the addon products as necessary to
|
Removes a list of items from a user's cart.
|
||||||
ensure the requested addon state.
|
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
:param addons: A list of dicts with the keys addon_to, item, variation
|
:param addons: A list of dicts with the keys addon_to, item, variation
|
||||||
:param add_to_cart_items: A list of dicts with the keys item, variation, count, custom_price, voucher, seat ID
|
:param session: Session ID of a guest
|
||||||
:param cart_id: The cart ID of the cart to modify
|
|
||||||
"""
|
"""
|
||||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
ia = False
|
ia = False
|
||||||
@@ -1653,7 +1635,6 @@ def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: L
|
|||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
|
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
|
||||||
cm.set_addons(addons)
|
cm.set_addons(addons)
|
||||||
cm.add_new_items(add_to_cart_items)
|
|
||||||
cm.commit()
|
cm.commit()
|
||||||
except LockTimeoutException:
|
except LockTimeoutException:
|
||||||
self.retry()
|
self.retry()
|
||||||
|
|||||||
@@ -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, periodic_task
|
from pretix.base.signals import checkin_created, order_placed, 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,6 +1154,23 @@ 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):
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user