forked from CGM_Public/pretix_original
Compare commits
168
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10bc49bc3a | ||
|
|
5aba1f9a23 | ||
|
|
a4eed87396 | ||
|
|
08879d0d55 | ||
|
|
c276a19bcc | ||
|
|
1e3c6e0b68 | ||
|
|
4e283eb560 | ||
|
|
52a1983630 | ||
|
|
3d85d9d865 | ||
|
|
4ca9a43890 | ||
|
|
d8bac7db65 | ||
|
|
91de0f93e6 | ||
|
|
901565203b | ||
|
|
14c6c9c0d7 | ||
|
|
6de6cf6c08 | ||
|
|
29306b3a4d | ||
|
|
ca69996611 | ||
|
|
16419b6ae4 | ||
|
|
d6258b9b54 | ||
|
|
6f75608196 | ||
|
|
6ef88e009b | ||
|
|
957100a195 | ||
|
|
112ef0908f | ||
|
|
91aaff7359 | ||
|
|
8ab61e2c38 | ||
|
|
c8ba5cc427 | ||
|
|
5ebad31b7d | ||
|
|
0429377f7d | ||
|
|
76e4b797a1 | ||
|
|
5f0009c996 | ||
|
|
de63a4be01 | ||
|
|
f3432139cb | ||
|
|
0b82ac9115 | ||
|
|
eb685b5141 | ||
|
|
5f7f0bd8f1 | ||
|
|
9fcef2dcaa | ||
|
|
fc3b186b93 | ||
|
|
a406884575 | ||
|
|
57ccd5f289 | ||
|
|
f4ac7e7f65 | ||
|
|
81d7045b31 | ||
|
|
f9502a3212 | ||
|
|
a31f624417 | ||
|
|
3f99e0bece | ||
|
|
7e64f2b38a | ||
|
|
ee2bc93608 | ||
|
|
fb4bed9d0d | ||
|
|
aec75e4d0c | ||
|
|
e7e41470fb | ||
|
|
0aa9dda90a | ||
|
|
d97c983b6f | ||
|
|
6c957f31ca | ||
|
|
8e6b4b3ec7 | ||
|
|
b24de62f73 | ||
|
|
cdbd220a12 | ||
|
|
2f11aee512 | ||
|
|
8ea475ce39 | ||
|
|
b29bc9db96 | ||
|
|
6bd6694132 | ||
|
|
110e6e248e | ||
|
|
985f4d969d | ||
|
|
826bd07b01 | ||
|
|
3e4e86742a | ||
|
|
ef5fcde5d9 | ||
|
|
8f1d53d016 | ||
|
|
9ca1573fcf | ||
|
|
5795aa6492 | ||
|
|
6e0613a2af | ||
|
|
b43ed38483 | ||
|
|
f0fedf0001 | ||
|
|
19373b8f91 | ||
|
|
45fd13786a | ||
|
|
ae5111ee7e | ||
|
|
d8bf3065d0 | ||
|
|
54f077665c | ||
|
|
482a66c546 | ||
|
|
e4cef6e46b | ||
|
|
cbee1b71fe | ||
|
|
0cd1290624 | ||
|
|
565f5e2ea7 | ||
|
|
b46c0eba0c | ||
|
|
39c3aef7bc | ||
|
|
cf3087453c | ||
|
|
7a870ee521 | ||
|
|
3922290633 | ||
|
|
8aa13d7e3e | ||
|
|
22e9a6eb92 | ||
|
|
2b6f82502e | ||
|
|
a10bf2a939 | ||
|
|
a80b7087d9 | ||
|
|
4b143e98eb | ||
|
|
bdb8b597d0 | ||
|
|
b1c9f40bc8 | ||
|
|
a3b6a008b5 | ||
|
|
9ce05e5cb9 | ||
|
|
f306527981 | ||
|
|
3e17ff9faa | ||
|
|
2a16cd4655 | ||
|
|
d1078da5bf | ||
|
|
483e7bc4ad | ||
|
|
401218b0a3 | ||
|
|
19175258fd | ||
|
|
22c36b89da | ||
|
|
2697ed0c5d | ||
|
|
f81d820a02 | ||
|
|
f8df66e621 | ||
|
|
2d9bfc80dc | ||
|
|
17b2e95569 | ||
|
|
e49f938eb3 | ||
|
|
8d63906341 | ||
|
|
cfefa1aad0 | ||
|
|
1d16049dc5 | ||
|
|
8452899edd | ||
|
|
d67ebc0f80 | ||
|
|
0e87f03e1e | ||
|
|
868408ea55 | ||
|
|
fc75cd35f8 | ||
|
|
a3e2540331 | ||
|
|
99ce7effde | ||
|
|
0d645fc4c5 | ||
|
|
359df1f51e | ||
|
|
7607cc5d2f | ||
|
|
40c8d014df | ||
|
|
c10efc692d | ||
|
|
8f0a277c7b | ||
|
|
9dc38e42d8 | ||
|
|
bfd88d1496 | ||
|
|
be6bd501bd | ||
|
|
d160c9fd67 | ||
|
|
221f14cc21 | ||
|
|
1dda2eb4fb | ||
|
|
30f2e99020 | ||
|
|
8efe276ed0 | ||
|
|
61b25acdd2 | ||
|
|
6cc9529d9a | ||
|
|
cdc5401dc2 | ||
|
|
1334a570e4 | ||
|
|
7a66aea2cb | ||
|
|
ee77a5e447 | ||
|
|
827e127568 | ||
|
|
ce0e0d7fd1 | ||
|
|
152a956dc5 | ||
|
|
68e2c355e6 | ||
|
|
171615558f | ||
|
|
a1765910ea | ||
|
|
417277958b | ||
|
|
0d50494e89 | ||
|
|
c6f634ce72 | ||
|
|
adc78c14ab | ||
|
|
b4ca2bdbb4 | ||
|
|
9a7ff592af | ||
|
|
548b54cca6 | ||
|
|
e736791446 | ||
|
|
7bd945b2e6 | ||
|
|
a07d5aaf05 | ||
|
|
0cf1a32902 | ||
|
|
be6aae8577 | ||
|
|
fe80f5fb78 | ||
|
|
a2c15ad89e | ||
|
|
cab0f37830 | ||
|
|
0423980058 | ||
|
|
63983b1b68 | ||
|
|
61241c2a1e | ||
|
|
4069c61054 | ||
|
|
9bf4fb2d0f | ||
|
|
ff910f293f | ||
|
|
74f7bec617 | ||
|
|
467a35e353 |
@@ -38,7 +38,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt update && sudo apt install gettext unzip
|
run: sudo apt update && sudo apt install -y gettext unzip
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: pip3 install -U setuptools build pip check-manifest
|
run: pip3 install -U setuptools build pip check-manifest
|
||||||
- name: Run check-manifest
|
- name: Run check-manifest
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
run: sudo apt update && sudo apt install enchant-2 hunspell aspell-en
|
run: sudo apt update && sudo apt install -y enchant-2 hunspell aspell-en
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -Ur requirements.txt
|
run: pip3 install -Ur requirements.txt
|
||||||
working-directory: ./doc
|
working-directory: ./doc
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
run: sudo apt update && sudo apt install gettext
|
run: sudo apt update && sudo apt -y install gettext
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -e ".[dev]"
|
run: pip3 install uv && uv pip install --system -e ".[dev]"
|
||||||
- name: Compile messages
|
- name: Compile messages
|
||||||
run: python manage.py compilemessages
|
run: python manage.py compilemessages
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
- name: Install system packages
|
- name: Install system packages
|
||||||
run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de
|
run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -e ".[dev]"
|
run: pip3 install uv && uv pip install --system -e ".[dev]"
|
||||||
- name: Spellcheck translations
|
- name: Spellcheck translations
|
||||||
run: potypo
|
run: potypo
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -e ".[dev]" psycopg2-binary
|
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||||
- name: Run isort
|
- name: Run isort
|
||||||
run: isort -c .
|
run: isort -c .
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install -e ".[dev]" psycopg2-binary
|
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||||
- name: Run flake8
|
- name: Run flake8
|
||||||
run: flake8 .
|
run: flake8 .
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
|
|||||||
+19
-13
@@ -30,15 +30,21 @@ jobs:
|
|||||||
python-version: "3.9"
|
python-version: "3.9"
|
||||||
- database: sqlite
|
- database: sqlite
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: pretix
|
||||||
|
options: >-
|
||||||
|
--health-cmd "pg_isready -U postgres -d pretix"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: harmon758/postgresql-action@v1
|
|
||||||
with:
|
|
||||||
postgresql version: '15'
|
|
||||||
postgresql db: 'pretix'
|
|
||||||
postgresql user: 'postgres'
|
|
||||||
postgresql password: 'postgres'
|
|
||||||
if: matrix.database == 'postgres'
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -50,9 +56,9 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: sudo apt update && sudo apt install gettext
|
run: sudo apt update && sudo apt install -y gettext
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: pip3 install --ignore-requires-python -e ".[dev]" psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
|
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||||
- name: Run checks
|
- name: Run checks
|
||||||
run: python manage.py check
|
run: python manage.py check
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
@@ -64,15 +70,15 @@ jobs:
|
|||||||
run: make all compress
|
run: make all compress
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
|
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml tests --maxfail=100
|
||||||
- name: Run concurrency tests
|
- name: Run concurrency tests
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
|
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reuse-db
|
||||||
if: matrix.database == 'postgres'
|
if: matrix.database == 'postgres'
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
file: src/coverage.xml
|
file: src/coverage.xml
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: false
|
||||||
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ tests:
|
|||||||
- cd src
|
- cd src
|
||||||
- python manage.py check
|
- python manage.py check
|
||||||
- make all compress
|
- make all compress
|
||||||
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
|
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
|
||||||
except:
|
except:
|
||||||
- pypi
|
- pypi
|
||||||
pypi:
|
pypi:
|
||||||
|
|||||||
@@ -294,6 +294,10 @@ Example::
|
|||||||
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``.
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +23,22 @@ 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.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -60,7 +76,10 @@ 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": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -102,7 +121,10 @@ 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
|
||||||
@@ -130,7 +152,10 @@ 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**:
|
||||||
@@ -147,7 +172,10 @@ 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
|
||||||
@@ -193,7 +221,10 @@ 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,8 +31,6 @@ subevent integer ID of the date
|
|||||||
position_count integer Number of tickets that match this list (read-only).
|
position_count integer Number of tickets that match this list (read-only).
|
||||||
checkin_count integer Number of check-ins performed on this list (read-only).
|
checkin_count integer Number of check-ins performed on this list (read-only).
|
||||||
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
||||||
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
|
|
||||||
**Deprecated, will be removed in pretix 2024.10.** Use :ref:`rest-autocheckinrules`: instead.
|
|
||||||
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
||||||
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
||||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||||
@@ -91,10 +89,7 @@ Endpoints
|
|||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"exit_all_at": null,
|
"exit_all_at": null,
|
||||||
"rules": {},
|
"rules": {},
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -146,10 +141,7 @@ Endpoints
|
|||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"exit_all_at": null,
|
"exit_all_at": null,
|
||||||
"rules": {},
|
"rules": {},
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -246,10 +238,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"allow_multiple_entries": false,
|
"allow_multiple_entries": false,
|
||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -271,10 +260,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"allow_multiple_entries": false,
|
"allow_multiple_entries": false,
|
||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
|
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
|
||||||
@@ -326,10 +312,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"allow_multiple_entries": false,
|
"allow_multiple_entries": false,
|
||||||
"allow_entry_after_exit": true,
|
"allow_entry_after_exit": true,
|
||||||
"addon_match": false,
|
"addon_match": false
|
||||||
"auto_checkin_sales_channels": [
|
|
||||||
"pretixpos"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
@@ -342,7 +325,7 @@ Endpoints
|
|||||||
|
|
||||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
|
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
|
||||||
|
|
||||||
Delete a check-in list. Note that this also deletes the information on all check-ins performed via this list.
|
Delete a check-in list. **Note that this also deletes the information on all check-ins performed via this list.**
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ url string The full URL to
|
|||||||
payments list of objects List of payment processes (see below)
|
payments list of objects List of payment processes (see below)
|
||||||
refunds list of objects List of refund processes (see below)
|
refunds list of objects List of refund processes (see below)
|
||||||
last_modified datetime Last modification of this object
|
last_modified datetime Last modification of this object
|
||||||
|
cancellation_date datetime Time of order cancellation (or ``null``). **Note**:
|
||||||
|
Will not be set for partial cancellations and is not
|
||||||
|
reliable for orders that have been cancelled,
|
||||||
|
reactivated and cancelled again.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -151,6 +155,9 @@ last_modified datetime Last modificati
|
|||||||
|
|
||||||
The ``expires`` attribute can now be passed during order creation.
|
The ``expires`` attribute can now be passed during order creation.
|
||||||
|
|
||||||
|
.. versionchanged:: 2024.11
|
||||||
|
|
||||||
|
The ``cancellation_date`` attribute has been added and can also be used as an ordering key.
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
@@ -206,6 +213,17 @@ checkins list of objects List of **succe
|
|||||||
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
|
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
|
||||||
├ device_id integer Attribute ``device_id`` of the device. Can be ``null``.
|
├ device_id integer Attribute ``device_id`` of the device. Can be ``null``.
|
||||||
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
|
||||||
|
print_logs list of objects List of print jobs recorded e.g. by the pretix apps
|
||||||
|
├ id integer Internal ID of the print job
|
||||||
|
├ successful boolean Whether the print job successfully resulted in a print.
|
||||||
|
This is not expected to be 100 % reliable information (since
|
||||||
|
printer feedback is never perfect) and there is no guarantee
|
||||||
|
that unsuccessful jobs will be logged.
|
||||||
|
├ device_id integer Attribute ``device_id`` of the device that recorded the print. Can be ``null``.
|
||||||
|
├ datetime datetime Time of printing
|
||||||
|
├ source string Source of print job, e.g. name of the app used.
|
||||||
|
├ type string Type of print (currently ``badge``, ``ticket``, ``certificate``, or ``other``)
|
||||||
|
└ info object Additional data with client-dependent structure.
|
||||||
downloads list of objects List of ticket download options
|
downloads list of objects List of ticket download options
|
||||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||||
└ url string Download URL
|
└ url string Download URL
|
||||||
@@ -233,6 +251,10 @@ 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.
|
||||||
|
|
||||||
.. _order-payment-resource:
|
.. _order-payment-resource:
|
||||||
|
|
||||||
Order payment resource
|
Order payment resource
|
||||||
@@ -399,10 +421,21 @@ List of all orders
|
|||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
"device": 2,
|
"device": 2,
|
||||||
|
"device_id": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
"auto_checked_in": false
|
"auto_checked_in": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"print_logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "badge",
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"device_id": 1,
|
||||||
|
"source": "pretixSCAN",
|
||||||
|
"info": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
@@ -438,14 +471,15 @@ List of all orders
|
|||||||
"provider": "banktransfer"
|
"provider": "banktransfer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refunds": []
|
"refunds": [],
|
||||||
|
"cancellation_date": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``,
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``,
|
||||||
``last_modified``, and ``status``. Default: ``datetime``
|
``last_modified``, ``status`` and ``cancellation_date``. Default: ``datetime``
|
||||||
:query string code: Only return orders that match the given order code
|
:query string code: Only return orders that match the given order code
|
||||||
:query string status: Only return orders in the given order status (see above)
|
:query string status: Only return orders in the given order status (see above)
|
||||||
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
|
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
|
||||||
@@ -626,10 +660,22 @@ Fetching individual orders
|
|||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
"device": 2,
|
"device": 2,
|
||||||
|
"device_id": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
"auto_checked_in": false
|
"auto_checked_in": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"print_logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "badge",
|
||||||
|
"successful": true,
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"device_id": 1,
|
||||||
|
"source": "pretixSCAN",
|
||||||
|
"info": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
@@ -665,7 +711,8 @@ Fetching individual orders
|
|||||||
"provider": "banktransfer"
|
"provider": "banktransfer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refunds": []
|
"refunds": [],
|
||||||
|
"cancellation_date": null
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -977,8 +1024,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``
|
||||||
|
|
||||||
@@ -1581,10 +1628,22 @@ List of all order positions
|
|||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
"device": 2,
|
"device": 2,
|
||||||
|
"device_id": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
"auto_checked_in": false
|
"auto_checked_in": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"print_logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "badge",
|
||||||
|
"successful": true,
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"device_id": 1,
|
||||||
|
"source": "pretixSCAN",
|
||||||
|
"info": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
@@ -1695,10 +1754,22 @@ Fetching individual positions
|
|||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
"device": 2,
|
"device": 2,
|
||||||
|
"device_id": 1,
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
"auto_checked_in": false
|
"auto_checked_in": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"print_logs": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "badge",
|
||||||
|
"successful": true,
|
||||||
|
"datetime": "2017-12-25T12:45:23Z",
|
||||||
|
"device_id": 1,
|
||||||
|
"source": "pretixSCAN",
|
||||||
|
"info": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
"answers": [
|
"answers": [
|
||||||
{
|
{
|
||||||
"question": 12,
|
"question": 12,
|
||||||
@@ -1795,6 +1866,10 @@ Manipulating individual positions
|
|||||||
|
|
||||||
The endpoints to manage blocks have been added.
|
The endpoints to manage blocks have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2024.9
|
||||||
|
|
||||||
|
The API now supports logging ticket and badge prints.
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
|
||||||
|
|
||||||
Updates specific fields on an order position. Currently, only the following fields are supported:
|
Updates specific fields on an order position. Currently, only the following fields are supported:
|
||||||
@@ -2054,6 +2129,59 @@ Manipulating individual positions
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position.
|
||||||
|
|
||||||
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/printlog/
|
||||||
|
|
||||||
|
Creates a print log, stating that this ticket has been printed.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/printlog/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"datetime": "2024-09-19T13:37:00+02:00",
|
||||||
|
"source": "pretixPOS",
|
||||||
|
"type": "badge",
|
||||||
|
"info": {
|
||||||
|
"cashier": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 201 Created
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/pdf
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1234,
|
||||||
|
"device_id": null,
|
||||||
|
"datetime": "2024-09-19T13:37:00+02:00",
|
||||||
|
"source": "pretixPOS",
|
||||||
|
"type": "badge",
|
||||||
|
"info": {
|
||||||
|
"cashier": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:param organizer: The ``slug`` field of the organizer to create a log for
|
||||||
|
:param event: The ``slug`` field of the event to create a log for
|
||||||
|
:param id: The ``id`` field of the order position to create a log for
|
||||||
|
:statuscode 201: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
|
||||||
|
**or** downloads are not available for this order position at this time. The response content will
|
||||||
|
contain more details.
|
||||||
|
:statuscode 404: The requested order position or download provider does not exist.
|
||||||
|
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
|
||||||
|
seconds.
|
||||||
|
|
||||||
Changing order contents
|
Changing order contents
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Endpoints
|
|||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"identifier": "web",
|
"identifier": "web",
|
||||||
"name": {
|
"label": {
|
||||||
"en": "Online shop"
|
"en": "Online shop"
|
||||||
},
|
},
|
||||||
"type": "web",
|
"type": "web",
|
||||||
@@ -88,7 +88,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "web",
|
"identifier": "web",
|
||||||
"name": {
|
"label": {
|
||||||
"en": "Online shop"
|
"en": "Online shop"
|
||||||
},
|
},
|
||||||
"type": "web",
|
"type": "web",
|
||||||
@@ -116,7 +116,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "api.custom",
|
"identifier": "api.custom",
|
||||||
"name": {
|
"label": {
|
||||||
"en": "Custom integration"
|
"en": "Custom integration"
|
||||||
},
|
},
|
||||||
"type": "api",
|
"type": "api",
|
||||||
@@ -133,7 +133,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "api.custom",
|
"identifier": "api.custom",
|
||||||
"name": {
|
"label": {
|
||||||
"en": "Custom integration"
|
"en": "Custom integration"
|
||||||
},
|
},
|
||||||
"type": "api",
|
"type": "api",
|
||||||
@@ -178,7 +178,7 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "web",
|
"identifier": "web",
|
||||||
"name": {
|
"label": {
|
||||||
"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-reusablemedia`:
|
.. _`rest-seats`:
|
||||||
|
|
||||||
Seats
|
Seats
|
||||||
=====
|
=====
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ 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.
|
||||||
@@ -467,6 +468,7 @@ 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.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ First, you need to declare that you are using non-essential cookies by respondin
|
|||||||
signal:
|
signal:
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
:no-index:
|
||||||
:members: register_cookie_providers
|
:members: register_cookie_providers
|
||||||
|
|
||||||
You are expected to return a list of ``CookieProvider`` objects instantiated from the following class:
|
You are expected to return a list of ``CookieProvider`` objects instantiated from the following class:
|
||||||
|
|||||||
@@ -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
|
register_text_placeholders, register_mail_placeholders, device_info_updated
|
||||||
|
|
||||||
Order events
|
Order events
|
||||||
""""""""""""
|
""""""""""""
|
||||||
@@ -22,12 +22,14 @@ Order events
|
|||||||
There are multiple signals that will be sent out in the ordering cycle:
|
There are multiple signals that will be sent out in the ordering cycle:
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
||||||
|
|
||||||
Check-ins
|
Check-ins
|
||||||
"""""""""
|
"""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: checkin_created
|
:members: checkin_created
|
||||||
|
|
||||||
|
|
||||||
@@ -39,18 +41,21 @@ Frontend
|
|||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
:no-index:
|
||||||
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
|
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
|
||||||
|
|
||||||
Request flow
|
Request flow
|
||||||
""""""""""""
|
""""""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
:no-index:
|
||||||
:members: process_request, process_response
|
:members: process_request, process_response
|
||||||
|
|
||||||
Vouchers
|
Vouchers
|
||||||
""""""""
|
""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
:no-index:
|
||||||
:members: voucher_redeem_info
|
:members: voucher_redeem_info
|
||||||
|
|
||||||
Backend
|
Backend
|
||||||
@@ -62,24 +67,28 @@ Backend
|
|||||||
item_formsets, order_search_filter_q, order_search_forms
|
item_formsets, order_search_filter_q, order_search_forms
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
|
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
|
||||||
|
|
||||||
Vouchers
|
Vouchers
|
||||||
""""""""
|
""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
|
:no-index:
|
||||||
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
|
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
|
||||||
|
|
||||||
Dashboards
|
Dashboards
|
||||||
""""""""""
|
""""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.control.signals
|
.. automodule:: pretix.control.signals
|
||||||
|
:no-index:
|
||||||
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
|
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
|
||||||
|
|
||||||
Ticket designs
|
Ticket designs
|
||||||
""""""""""""""
|
""""""""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: layout_text_variables, layout_image_variables
|
:members: layout_text_variables, layout_image_variables
|
||||||
|
|
||||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
||||||
@@ -89,4 +98,5 @@ 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
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ that we'll provide in this plugin:
|
|||||||
Similar signals exist for other objects:
|
Similar signals exist for other objects:
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: voucher_import_columns
|
:members: voucher_import_columns
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -84,8 +84,6 @@ convenient to you:
|
|||||||
|
|
||||||
.. automethod:: _register_fonts
|
.. automethod:: _register_fonts
|
||||||
|
|
||||||
.. automethod:: _register_event_fonts
|
|
||||||
|
|
||||||
.. automethod:: _on_first_page
|
.. automethod:: _on_first_page
|
||||||
|
|
||||||
.. automethod:: _on_other_page
|
.. automethod:: _on_other_page
|
||||||
|
|||||||
@@ -86,7 +86,10 @@ Signals
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: register_text_placeholders
|
:members: register_text_placeholders
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
|
:no-index:
|
||||||
:members: register_mail_placeholders
|
:members: register_mail_placeholders
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
KulturPass
|
KulturPass
|
||||||
=========
|
==========
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ expects and - more importantly - supports.
|
|||||||
for a sample configuration in an academic context.
|
for a sample configuration in an academic context.
|
||||||
|
|
||||||
Note, that you can have multiple attributes with the same ``friendlyName``
|
Note, that you can have multiple attributes with the same ``friendlyName``
|
||||||
but different ``name``s. This is often used in systems, where the same
|
but different ``name`` value. This is often used in systems, where the same
|
||||||
information (for example a persons name) is saved in different fields -
|
information (for example a persons name) is saved in different fields -
|
||||||
for example because one institution is returning SAML 1.0 and other
|
for example because one institution is returning SAML 1.0 and other
|
||||||
institutions are returning SAML 2.0 style attributes. Typically, this only
|
institutions are returning SAML 2.0 style attributes. Typically, this only
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ item_assignments list of objects Products this l
|
|||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Layout endpoints
|
||||||
---------
|
----------------
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
||||||
|
|
||||||
@@ -268,5 +268,75 @@ 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
|
||||||
|
|||||||
+11
-13
@@ -43,7 +43,7 @@ dependencies = [
|
|||||||
"django-formset-js-improved==0.5.0.3",
|
"django-formset-js-improved==0.5.0.3",
|
||||||
"django-formtools==2.5.1",
|
"django-formtools==2.5.1",
|
||||||
"django-hierarkey==1.2.*",
|
"django-hierarkey==1.2.*",
|
||||||
"django-hijack==3.6.*",
|
"django-hijack==3.7.*",
|
||||||
"django-i18nfield==1.9.*,>=1.9.4",
|
"django-i18nfield==1.9.*,>=1.9.4",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
"django-localflavor==4.0",
|
"django-localflavor==4.0",
|
||||||
@@ -55,7 +55,7 @@ dependencies = [
|
|||||||
"django-scopes==2.0.*",
|
"django-scopes==2.0.*",
|
||||||
"django-statici18n==2.5.*",
|
"django-statici18n==2.5.*",
|
||||||
"djangorestframework==3.15.*",
|
"djangorestframework==3.15.*",
|
||||||
"dnspython==2.6.*",
|
"dnspython==2.7.*",
|
||||||
"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+
|
||||||
@@ -74,26 +74,25 @@ dependencies = [
|
|||||||
"paypal-checkout-serversdk==1.0.*",
|
"paypal-checkout-serversdk==1.0.*",
|
||||||
"PyJWT==2.9.*",
|
"PyJWT==2.9.*",
|
||||||
"phonenumberslite==8.13.*",
|
"phonenumberslite==8.13.*",
|
||||||
"Pillow==10.4.*",
|
"Pillow==11.0.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==5.28.*",
|
"protobuf==5.28.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.22",
|
"pycparser==2.22",
|
||||||
"pycryptodome==3.20.*",
|
"pycryptodome==3.21.*",
|
||||||
"pypdf==5.0.*",
|
"pypdf==5.1.*",
|
||||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||||
"python-dateutil==2.9.*",
|
"python-dateutil==2.9.*",
|
||||||
"pytz",
|
"pytz",
|
||||||
"pytz-deprecation-shim==0.1.*",
|
"pytz-deprecation-shim==0.1.*",
|
||||||
"pyuca",
|
"pyuca",
|
||||||
"qrcode==7.4.*",
|
"qrcode==8.0",
|
||||||
"redis==5.0.*",
|
"redis==5.2.*",
|
||||||
"reportlab==4.2.*",
|
"reportlab==4.2.*",
|
||||||
"requests==2.31.*",
|
"requests==2.31.*",
|
||||||
"sentry-sdk==2.14.*",
|
"sentry-sdk==2.18.*",
|
||||||
"sepaxml==2.6.*",
|
"sepaxml==2.6.*",
|
||||||
"slimit",
|
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
"tlds>=2020041600",
|
"tlds>=2020041600",
|
||||||
@@ -102,7 +101,7 @@ dependencies = [
|
|||||||
"vat_moss_forked==2020.3.20.0.11.0",
|
"vat_moss_forked==2020.3.20.0.11.0",
|
||||||
"vobject==0.9.*",
|
"vobject==0.9.*",
|
||||||
"webauthn==2.2.*",
|
"webauthn==2.2.*",
|
||||||
"zeep==4.2.*"
|
"zeep==4.3.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@@ -111,18 +110,17 @@ dev = [
|
|||||||
"aiohttp==3.10.*",
|
"aiohttp==3.10.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.24.*",
|
"fakeredis==2.26.*",
|
||||||
"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",
|
"pytest-asyncio>=0.24",
|
||||||
"pytest-cache",
|
"pytest-cache",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-django==4.*",
|
"pytest-django==4.*",
|
||||||
"pytest-mock==3.14.*",
|
"pytest-mock==3.14.*",
|
||||||
"pytest-rerunfailures==14.*",
|
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.6.*",
|
"pytest-xdist==3.6.*",
|
||||||
"pytest==8.3.*",
|
"pytest==8.3.*",
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2024.9.0.dev0"
|
__version__ = "2024.11.0.dev0"
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:blockedsecrets-list'),
|
('GET', 'api-v1:blockedsecrets-list'),
|
||||||
('GET', 'api-v1:order-list'),
|
('GET', 'api-v1:order-list'),
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
('GET', 'api-v1:orderposition-pdf_image'),
|
||||||
|
('POST', 'api-v1:orderposition-printlog'),
|
||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
@@ -112,6 +113,7 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:revokedsecrets-list'),
|
('GET', 'api-v1:revokedsecrets-list'),
|
||||||
('GET', 'api-v1:blockedsecrets-list'),
|
('GET', 'api-v1:blockedsecrets-list'),
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
('GET', 'api-v1:orderposition-pdf_image'),
|
||||||
|
('POST', 'api-v1:orderposition-printlog'),
|
||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
@@ -147,6 +149,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:revokedsecrets-list'),
|
('GET', 'api-v1:revokedsecrets-list'),
|
||||||
('GET', 'api-v1:blockedsecrets-list'),
|
('GET', 'api-v1:blockedsecrets-list'),
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
('GET', 'api-v1:orderposition-pdf_image'),
|
||||||
|
('POST', 'api-v1:orderposition-printlog'),
|
||||||
('GET', 'api-v1:event.settings'),
|
('GET', 'api-v1:event.settings'),
|
||||||
('POST', 'api-v1:upload'),
|
('POST', 'api-v1:upload'),
|
||||||
('POST', 'api-v1:checkinrpc.redeem'),
|
('POST', 'api-v1:checkinrpc.redeem'),
|
||||||
@@ -188,6 +191,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
|||||||
('GET', 'api-v1:orderposition-list'),
|
('GET', 'api-v1:orderposition-list'),
|
||||||
('GET', 'api-v1:orderposition-answer'),
|
('GET', 'api-v1:orderposition-answer'),
|
||||||
('GET', 'api-v1:orderposition-pdf_image'),
|
('GET', 'api-v1:orderposition-pdf_image'),
|
||||||
|
('POST', 'api-v1:orderposition-printlog'),
|
||||||
('POST', 'api-v1:order-mark-canceled'),
|
('POST', 'api-v1:order-mark-canceled'),
|
||||||
('POST', 'api-v1:orderpayment-list'),
|
('POST', 'api-v1:orderpayment-list'),
|
||||||
('POST', 'api-v1:orderrefund-list'),
|
('POST', 'api-v1:orderrefund-list'),
|
||||||
|
|||||||
@@ -88,16 +88,20 @@ 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({
|
||||||
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
"limit_sales_channels": [
|
||||||
"the list of all sales channels."
|
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||||
)
|
"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({
|
||||||
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
"limit_sales_channels": [
|
||||||
"the same list."
|
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||||
)
|
"the same list."
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
if data["sales_channels"] == all_channels:
|
if data["sales_channels"] == all_channels:
|
||||||
data["all_sales_channels"] = True
|
data["all_sales_channels"] = True
|
||||||
@@ -106,6 +110,10 @@ class SalesChannelMigrationMixin:
|
|||||||
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):
|
||||||
|
|||||||
@@ -26,31 +26,22 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import Checkin, CheckinList, SalesChannel
|
from pretix.base.models import Checkin, CheckinList
|
||||||
|
|
||||||
|
|
||||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||||
checkin_count = serializers.IntegerField(read_only=True)
|
checkin_count = serializers.IntegerField(read_only=True)
|
||||||
position_count = serializers.IntegerField(read_only=True)
|
position_count = serializers.IntegerField(read_only=True)
|
||||||
auto_checkin_sales_channels = serializers.SlugRelatedField(
|
|
||||||
slug_field="identifier",
|
|
||||||
queryset=SalesChannel.objects.none(),
|
|
||||||
required=False,
|
|
||||||
allow_empty=True,
|
|
||||||
many=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||||
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
|
'include_pending', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||||
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
|
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
|
||||||
|
|
||||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||||
|
|
||||||
|
|||||||
@@ -772,6 +772,7 @@ 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',
|
||||||
@@ -916,6 +917,7 @@ 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',
|
||||||
|
|||||||
@@ -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:
|
if limit_sales_channels and not validated_data.get('all_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,7 +441,22 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemCategory
|
model = ItemCategory
|
||||||
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon')
|
fields = (
|
||||||
|
'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,
|
||||||
RevokedTicketSecret,
|
PrintLog, RevokedTicketSecret,
|
||||||
)
|
)
|
||||||
from pretix.base.pdf import get_images, get_variables
|
from pretix.base.pdf import get_images, get_variables
|
||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
@@ -284,6 +284,26 @@ class CheckinSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
||||||
|
|
||||||
|
|
||||||
|
class PrintLogSerializer(serializers.ModelSerializer):
|
||||||
|
device_id = serializers.SlugRelatedField(
|
||||||
|
source='device',
|
||||||
|
slug_field='device_id',
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PrintLog
|
||||||
|
fields = (
|
||||||
|
"id",
|
||||||
|
"successful",
|
||||||
|
"datetime",
|
||||||
|
"source",
|
||||||
|
"type",
|
||||||
|
"device_id",
|
||||||
|
"info",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
class FailedCheckinSerializer(I18nAwareModelSerializer):
|
||||||
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
|
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
|
||||||
raw_barcode = serializers.CharField(required=True, allow_null=False)
|
raw_barcode = serializers.CharField(required=True, allow_null=False)
|
||||||
@@ -476,6 +496,7 @@ class OrderPositionListSerializer(serializers.ListSerializer):
|
|||||||
|
|
||||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||||
checkins = CheckinSerializer(many=True, read_only=True)
|
checkins = CheckinSerializer(many=True, read_only=True)
|
||||||
|
print_logs = PrintLogSerializer(many=True, read_only=True)
|
||||||
answers = AnswerSerializer(many=True)
|
answers = AnswerSerializer(many=True)
|
||||||
downloads = PositionDownloadsField(source='*', read_only=True)
|
downloads = PositionDownloadsField(source='*', read_only=True)
|
||||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||||
@@ -490,7 +511,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
||||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||||
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
'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',
|
||||||
@@ -577,9 +598,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
|||||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
|
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat',
|
||||||
'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
|
'require_attention', 'order__status', 'order__valid_if_pending', 'order__require_approval',
|
||||||
'blocked')
|
'valid_from', 'valid_until', 'blocked')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -732,12 +753,12 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||||
'url', 'customer', 'valid_if_pending', 'api_meta'
|
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date'
|
||||||
)
|
)
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
||||||
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -1494,6 +1515,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
pos.answers = answers
|
pos.answers = answers
|
||||||
pos.pseudonymization_id = "PREVIEW"
|
pos.pseudonymization_id = "PREVIEW"
|
||||||
pos.checkins = []
|
pos.checkins = []
|
||||||
|
pos.print_logs = []
|
||||||
pos_map[pos.positionid] = pos
|
pos_map[pos.positionid] = pos
|
||||||
else:
|
else:
|
||||||
if pos.voucher:
|
if pos.voucher:
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ from pretix.base.models import (
|
|||||||
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
|
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
|
||||||
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
||||||
)
|
)
|
||||||
|
from pretix.base.models.orders import PrintLog
|
||||||
from pretix.base.services.checkin import (
|
from pretix.base.services.checkin import (
|
||||||
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||||
)
|
)
|
||||||
@@ -115,7 +116,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
if 'subevent' in self.request.query_params.getlist('expand'):
|
if 'subevent' in self.request.query_params.getlist('expand'):
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
||||||
'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
|
'subevent__seat_category_mappings', 'subevent__meta_values',
|
||||||
)
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -142,7 +143,9 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
data=self.request.data
|
data=self.request.data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
|
instance.checkins.all().delete()
|
||||||
instance.log_action(
|
instance.log_action(
|
||||||
'pretix.event.checkinlist.deleted',
|
'pretix.event.checkinlist.deleted',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
@@ -365,8 +368,9 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
lookup='checkins',
|
lookup='checkins',
|
||||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
|
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||||
),
|
),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||||
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||||
@@ -378,6 +382,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
'positions',
|
'positions',
|
||||||
OrderPosition.objects.prefetch_related(
|
OrderPosition.objects.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -389,8 +394,9 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
lookup='checkins',
|
lookup='checkins',
|
||||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
|
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||||
),
|
),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import base64
|
import base64
|
||||||
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from cryptography.hazmat.backends.openssl.backend import Backend
|
from cryptography.hazmat.backends.openssl.backend import Backend
|
||||||
@@ -146,6 +147,8 @@ class InitializeView(APIView):
|
|||||||
permission_classes = ()
|
permission_classes = ()
|
||||||
|
|
||||||
def post(self, request, format=None):
|
def post(self, request, format=None):
|
||||||
|
from pretix.base.signals import device_info_updated
|
||||||
|
|
||||||
serializer = InitializationRequestSerializer(data=request.data)
|
serializer = InitializationRequestSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
@@ -160,6 +163,8 @@ class InitializeView(APIView):
|
|||||||
if device.revoked:
|
if device.revoked:
|
||||||
raise ValidationError({'token': ['This initialization token has been revoked.']})
|
raise ValidationError({'token': ['This initialization token has been revoked.']})
|
||||||
|
|
||||||
|
old_instance = copy.copy(device)
|
||||||
|
|
||||||
device.initialized = now()
|
device.initialized = now()
|
||||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
@@ -174,6 +179,10 @@ class InitializeView(APIView):
|
|||||||
|
|
||||||
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
|
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
|
||||||
|
|
||||||
|
device_info_updated.send(
|
||||||
|
sender=Device, old_device=old_instance, new_device=device
|
||||||
|
)
|
||||||
|
|
||||||
serializer = DeviceSerializer(device)
|
serializer = DeviceSerializer(device)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@@ -182,9 +191,12 @@ class UpdateView(APIView):
|
|||||||
authentication_classes = (DeviceTokenAuthentication,)
|
authentication_classes = (DeviceTokenAuthentication,)
|
||||||
|
|
||||||
def post(self, request, format=None):
|
def post(self, request, format=None):
|
||||||
|
from pretix.base.signals import device_info_updated
|
||||||
|
|
||||||
serializer = UpdateRequestSerializer(data=request.data)
|
serializer = UpdateRequestSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
device = request.auth
|
device = request.auth
|
||||||
|
old_instance = copy.copy(device)
|
||||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
device.os_name = serializer.validated_data.get('os_name')
|
device.os_name = serializer.validated_data.get('os_name')
|
||||||
@@ -200,6 +212,10 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@@ -297,7 +297,8 @@ 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']
|
||||||
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
if not new_event.all_sales_channels:
|
||||||
|
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
||||||
else:
|
else:
|
||||||
serializer.instance.set_defaults()
|
serializer.instance.set_defaults()
|
||||||
|
|
||||||
@@ -370,7 +371,7 @@ with scopes_disabled():
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SubEvent
|
model = SubEvent
|
||||||
fields = ['active', 'event__live']
|
fields = ['is_public', 'active', 'event__live']
|
||||||
|
|
||||||
def ends_after_qs(self, queryset, name, value):
|
def ends_after_qs(self, queryset, name, value):
|
||||||
expr = Q(
|
expr = Q(
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from pretix.base.models import (
|
|||||||
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
|
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
|
||||||
ReusableMedium,
|
ReusableMedium,
|
||||||
)
|
)
|
||||||
|
from pretix.base.models.orders import PrintLog
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
|||||||
'order', 'order__event', 'order__event__organizer', 'seat',
|
'order', 'order__event', 'order__event__organizer', 'seat',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ from pretix.api.serializers.order import (
|
|||||||
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
||||||
OrderPositionSerializer, OrderRefundCreateSerializer,
|
OrderPositionSerializer, OrderRefundCreateSerializer,
|
||||||
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
||||||
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
|
PrintLogSerializer, RevokedTicketSecretSerializer,
|
||||||
|
SimulatedOrderSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.orderchange import (
|
from pretix.api.serializers.orderchange import (
|
||||||
BlockNameSerializer, OrderChangeOperationSerializer,
|
BlockNameSerializer, OrderChangeOperationSerializer,
|
||||||
@@ -75,7 +76,7 @@ from pretix.base.models import (
|
|||||||
TeamAPIToken, generate_secret,
|
TeamAPIToken, generate_secret,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret,
|
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret,
|
||||||
)
|
)
|
||||||
from pretix.base.payment import PaymentException
|
from pretix.base.payment import PaymentException
|
||||||
from pretix.base.pdf import get_images
|
from pretix.base.pdf import get_images
|
||||||
@@ -214,7 +215,7 @@ class OrderViewSetMixin:
|
|||||||
queryset = Order.objects.none()
|
queryset = Order.objects.none()
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||||
ordering = ('datetime',)
|
ordering = ('datetime',)
|
||||||
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
|
ordering_fields = ('datetime', 'code', 'status', 'last_modified', 'cancellation_date')
|
||||||
filterset_class = OrderFilter
|
filterset_class = OrderFilter
|
||||||
lookup_field = 'code'
|
lookup_field = 'code'
|
||||||
|
|
||||||
@@ -259,6 +260,7 @@ class OrderViewSetMixin:
|
|||||||
'positions',
|
'positions',
|
||||||
opq.all().prefetch_related(
|
opq.all().prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
|
||||||
)),
|
)),
|
||||||
@@ -280,6 +282,7 @@ class OrderViewSetMixin:
|
|||||||
'positions',
|
'positions',
|
||||||
opq.all().prefetch_related(
|
opq.all().prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'item', 'variation',
|
'item', 'variation',
|
||||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
||||||
'seat',
|
'seat',
|
||||||
@@ -1093,6 +1096,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
Prefetch('item', queryset=self.request.event.items.prefetch_related(
|
||||||
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||||
to_attr='meta_values_cached')
|
to_attr='meta_values_cached')
|
||||||
@@ -1136,6 +1140,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
else:
|
else:
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
'answers', 'answers__options', 'answers__question',
|
'answers', 'answers__options', 'answers__question',
|
||||||
).select_related(
|
).select_related(
|
||||||
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
||||||
@@ -1254,6 +1259,34 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
|
||||||
|
def printlog(self, request, **kwargs):
|
||||||
|
pos = self.get_object()
|
||||||
|
serializer = PrintLogSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
serializer.save(
|
||||||
|
position=pos,
|
||||||
|
device=request.auth if isinstance(request.auth, Device) else None,
|
||||||
|
user=request.user if request.user.is_authenticated else None,
|
||||||
|
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
|
||||||
|
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
pos.order.log_action(
|
||||||
|
"pretix.event.order.print",
|
||||||
|
data={
|
||||||
|
"position": pos.pk,
|
||||||
|
"positionid": pos.positionid,
|
||||||
|
**serializer.validated_data,
|
||||||
|
},
|
||||||
|
auth=request.auth,
|
||||||
|
user=request.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
@action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)')
|
@action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)')
|
||||||
def pdf_image(self, request, key, **kwargs):
|
def pdf_image(self, request, key, **kwargs):
|
||||||
pos = self.get_object()
|
pos = self.get_object()
|
||||||
|
|||||||
@@ -1122,6 +1122,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
|
|
||||||
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']
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ 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
|
||||||
|
|
||||||
@@ -50,17 +51,23 @@ class Command(BaseCommand):
|
|||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
|
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
|
||||||
'(dotted path, comma separation)')
|
'(dotted path, comma separation)')
|
||||||
|
parser.add_argument('--list-tasks', action='store_true', help='Only list all tasks')
|
||||||
parser.add_argument('--exclude', action='store', type=str, help='Exclude the tasks with this name '
|
parser.add_argument('--exclude', action='store', type=str, help='Exclude the tasks with this name '
|
||||||
'(dotted path, comma separation)')
|
'(dotted path, comma separation)')
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
for receiver in periodic_task._live_receivers(self):
|
for receiver in periodic_task._live_receivers(self):
|
||||||
name = f'{receiver.__module__}.{receiver.__name__}'
|
name = f'{receiver.__module__}.{receiver.__name__}'
|
||||||
|
if options['list_tasks']:
|
||||||
|
print(name)
|
||||||
|
continue
|
||||||
if options.get('tasks'):
|
if options.get('tasks'):
|
||||||
if name not in options.get('tasks').split(','):
|
if name not in options.get('tasks').split(','):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2024-09-19 10:41
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||||
|
("pretixbase", "0271_itemcategory_cross_selling"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PrintLog",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("datetime", models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
("successful", models.BooleanField(default=True)),
|
||||||
|
("source", models.CharField(max_length=255)),
|
||||||
|
("type", models.CharField(max_length=255)),
|
||||||
|
("info", models.JSONField(default=dict)),
|
||||||
|
(
|
||||||
|
"api_token",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="pretixbase.teamapitoken",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"device",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="print_logs",
|
||||||
|
to="pretixbase.device",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"oauth_application",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"position",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="print_logs",
|
||||||
|
to="pretixbase.orderposition",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="print_logs",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ("-datetime",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2024-10-29 15:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_autocheckin(apps, schema_editor):
|
||||||
|
CheckinList = apps.get_model("pretixbase", "CheckinList")
|
||||||
|
AutoCheckinRule = apps.get_model("autocheckin", "AutoCheckinRule")
|
||||||
|
|
||||||
|
for cl in CheckinList.objects.filter(auto_checkin_sales_channels__isnull=False).select_related("event", "event__organizer"):
|
||||||
|
sales_channels = cl.auto_checkin_sales_channels.all()
|
||||||
|
all_sales_channels = cl.event.organizer.sales_channels.all()
|
||||||
|
|
||||||
|
if "pretix.plugins.autocheckin" not in cl.event.plugins:
|
||||||
|
cl.event.plugins = cl.event.plugins + ",pretix.plugins.autocheckin"
|
||||||
|
cl.event.save()
|
||||||
|
|
||||||
|
r = AutoCheckinRule.objects.get_or_create(
|
||||||
|
list=cl,
|
||||||
|
event=cl.event,
|
||||||
|
all_products=True,
|
||||||
|
all_payment_methods=True,
|
||||||
|
defaults=dict(
|
||||||
|
mode="placed",
|
||||||
|
all_sales_channels=len(sales_channels) == len(all_sales_channels),
|
||||||
|
)
|
||||||
|
)[0]
|
||||||
|
if len(sales_channels) != len(all_sales_channels):
|
||||||
|
r.limit_sales_channels.set(sales_channels)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0272_printlog"),
|
||||||
|
("autocheckin", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_autocheckin,
|
||||||
|
migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="checkinlist",
|
||||||
|
name="auto_checkin_sales_channels",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -571,13 +571,23 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
|
|
||||||
def get_session_auth_hash(self):
|
def get_session_auth_hash(self):
|
||||||
"""
|
"""
|
||||||
Return an HMAC that needs to
|
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):
|
||||||
|
"""
|
||||||
"""
|
"""
|
||||||
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).hexdigest()
|
return salted_hmac(key_salt, payload, secret=secret).hexdigest()
|
||||||
|
|
||||||
def update_session_token(self):
|
def update_session_token(self):
|
||||||
self.session_token = generate_session_token()
|
self.session_token = generate_session_token()
|
||||||
|
|||||||
@@ -99,14 +99,6 @@ class CheckinList(LoggedModel):
|
|||||||
verbose_name=_('Automatically check out everyone at'),
|
verbose_name=_('Automatically check out everyone at'),
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
auto_checkin_sales_channels = models.ManyToManyField(
|
|
||||||
"SalesChannel",
|
|
||||||
verbose_name=_('Sales channels to automatically check in'),
|
|
||||||
help_text=_('This option is deprecated and will be removed in the next months. As a replacement, our new plugin '
|
|
||||||
'"Auto check-in" can be used. When we remove this option, we will automatically migrate your event '
|
|
||||||
'to use the new plugin.'),
|
|
||||||
blank=True,
|
|
||||||
)
|
|
||||||
rules = models.JSONField(default=dict, blank=True)
|
rules = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='event__organizer')
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
@@ -141,7 +133,7 @@ class CheckinList(LoggedModel):
|
|||||||
return self.positions_query(ignore_status=False)
|
return self.positions_query(ignore_status=False)
|
||||||
|
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def positions_inside_query(self, ignore_status=False, at_time=None):
|
def _filter_positions_inside(self, qs, at_time=None):
|
||||||
if at_time is None:
|
if at_time is None:
|
||||||
c_q = []
|
c_q = []
|
||||||
else:
|
else:
|
||||||
@@ -149,7 +141,7 @@ class CheckinList(LoggedModel):
|
|||||||
|
|
||||||
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
|
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
|
||||||
# Use a simple approach that works on all databases
|
# Use a simple approach that works on all databases
|
||||||
qs = self.positions_query(ignore_status=ignore_status).annotate(
|
qs = qs.annotate(
|
||||||
last_entry=Subquery(
|
last_entry=Subquery(
|
||||||
Checkin.objects.filter(
|
Checkin.objects.filter(
|
||||||
*c_q,
|
*c_q,
|
||||||
@@ -202,7 +194,7 @@ class CheckinList(LoggedModel):
|
|||||||
.values("position_id", "type", "datetime", "cnt_exists_after")
|
.values("position_id", "type", "datetime", "cnt_exists_after")
|
||||||
.query.sql_with_params()
|
.query.sql_with_params()
|
||||||
)
|
)
|
||||||
return self.positions_query(ignore_status=ignore_status).filter(
|
return qs.filter(
|
||||||
pk__in=RawSQL(
|
pk__in=RawSQL(
|
||||||
f"""
|
f"""
|
||||||
SELECT "position_id"
|
SELECT "position_id"
|
||||||
@@ -214,6 +206,10 @@ class CheckinList(LoggedModel):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@scopes_disabled()
|
||||||
|
def positions_inside_query(self, ignore_status=False, at_time=None):
|
||||||
|
return self._filter_positions_inside(self.positions_query(ignore_status=ignore_status), at_time=at_time)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def positions_inside(self):
|
def positions_inside(self):
|
||||||
return self.positions_inside_query(None)
|
return self.positions_inside_query(None)
|
||||||
|
|||||||
@@ -219,13 +219,24 @@ 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).hexdigest()
|
return salted_hmac(key_salt, payload, secret=secret).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
|
||||||
|
|||||||
@@ -20,11 +20,11 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict, namedtuple
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from math import ceil
|
from math import ceil, inf
|
||||||
from typing import Dict, Optional, Tuple
|
from typing import Dict
|
||||||
|
|
||||||
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,6 +36,8 @@ 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'
|
||||||
@@ -245,22 +247,26 @@ class Discount(LoggedModel):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result):
|
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
|
||||||
if self.condition_min_value and sum(positions[idx][2] for idx in condition_idx_group) < self.condition_min_value:
|
if self.condition_min_value and sum(positions[idx].line_price_gross 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][2]
|
previous_price = positions[idx].line_price_gross
|
||||||
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
|
||||||
|
|
||||||
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result):
|
if collect_potential_discounts is not None:
|
||||||
|
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
|
||||||
|
|
||||||
@@ -268,23 +274,53 @@ 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:
|
||||||
if not self.condition_min_count:
|
# sort by line_price
|
||||||
raise ValueError('Validation invariant violated.')
|
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
|
||||||
|
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][2]
|
previous_price = positions[idx].line_price_gross
|
||||||
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,
|
||||||
@@ -292,15 +328,16 @@ 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][2])
|
result.setdefault(idx, positions[idx].line_price_gross)
|
||||||
|
|
||||||
def apply(self, positions: Dict[int, Tuple[int, Optional[int], Decimal, bool, Decimal]]) -> Dict[int, Decimal]:
|
def apply(self, positions: Dict[int, PositionInfo],
|
||||||
|
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 tuples of the form
|
:param positions: Dictionary mapping IDs to PositionInfo tuples.
|
||||||
``(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
|
||||||
@@ -342,13 +379,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)
|
self._apply_min_count(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
|
||||||
else:
|
else:
|
||||||
self._apply_min_value(positions, condition_candidates, benefit_candidates, result)
|
self._apply_min_value(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
|
||||||
|
|
||||||
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][1] or 0 # subevent_id
|
return positions[idx].subevent_id or 0
|
||||||
|
|
||||||
# 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
|
||||||
@@ -357,11 +394,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][1] == subevent_id]
|
benefit_g = [idx for idx in benefit_candidates if positions[idx].subevent_id == subevent_id]
|
||||||
if self.condition_min_count:
|
if self.condition_min_count:
|
||||||
self._apply_min_count(positions, g, benefit_g, result)
|
self._apply_min_count(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
|
||||||
else:
|
else:
|
||||||
self._apply_min_value(positions, g, benefit_g, result)
|
self._apply_min_value(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
|
||||||
|
|
||||||
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:
|
||||||
@@ -377,9 +414,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[1]].append(idx)
|
subevent_to_idx[p.subevent_id].append(idx)
|
||||||
for v in subevent_to_idx.values():
|
for v in subevent_to_idx.values():
|
||||||
v.sort(key=lambda idx: positions[idx][2])
|
v.sort(key=lambda idx: positions[idx].line_price_gross)
|
||||||
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
|
||||||
@@ -394,7 +431,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][1] for idx in current_group}:
|
if se not in {positions[idx].subevent_id for idx in current_group}:
|
||||||
candidates += l
|
candidates += l
|
||||||
cardinality = len(l)
|
cardinality = len(l)
|
||||||
|
|
||||||
@@ -403,7 +440,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][2])
|
candidates = sorted(candidates, key=lambda idx: positions[idx].line_price_gross)
|
||||||
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:
|
||||||
@@ -415,14 +452,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][1]].remove(c)
|
subevent_to_idx[positions[c].subevent_id].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][1] for idx in group}:
|
if se not in {positions[idx].subevent_id 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
|
||||||
@@ -432,6 +469,8 @@ 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
|
||||||
|
|||||||
@@ -870,10 +870,12 @@ 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
|
||||||
@@ -911,6 +913,9 @@ 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()
|
||||||
@@ -1019,10 +1024,9 @@ class Event(EventMixin, LoggedModel):
|
|||||||
|
|
||||||
checkin_list_map = {}
|
checkin_list_map = {}
|
||||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
|
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
|
||||||
'limit_products', 'auto_checkin_sales_channels'
|
'limit_products'
|
||||||
):
|
):
|
||||||
items = list(cl.limit_products.all())
|
items = list(cl.limit_products.all())
|
||||||
auto_checkin_sales_channels = list(cl.auto_checkin_sales_channels.all())
|
|
||||||
checkin_list_map[cl.pk] = cl
|
checkin_list_map[cl.pk] = cl
|
||||||
cl.pk = None
|
cl.pk = None
|
||||||
cl._prefetched_objects_cache = {}
|
cl._prefetched_objects_cache = {}
|
||||||
@@ -1034,8 +1038,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
cl.log_action('pretix.object.cloned')
|
cl.log_action('pretix.object.cloned')
|
||||||
for i in items:
|
for i in items:
|
||||||
cl.limit_products.add(item_map[i.pk])
|
cl.limit_products.add(item_map[i.pk])
|
||||||
if auto_checkin_sales_channels:
|
|
||||||
cl.auto_checkin_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in auto_checkin_sales_channels]))
|
|
||||||
|
|
||||||
if other.seating_plan:
|
if other.seating_plan:
|
||||||
if other.seating_plan.organizer_id == self.organizer_id:
|
if other.seating_plan.organizer_id == self.organizer_id:
|
||||||
|
|||||||
@@ -63,14 +63,13 @@ 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):
|
||||||
@@ -111,6 +110,33 @@ 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")
|
||||||
@@ -119,19 +145,31 @@ 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.is_addon:
|
if self.category_type != 'normal':
|
||||||
return _('{category} (Add-On products)').format(category=str(name))
|
return _('{category} ({category_type})').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 products')
|
return _('Add-on category')
|
||||||
|
elif self.cross_selling_mode:
|
||||||
|
return self.get_cross_selling_mode_display()
|
||||||
else:
|
else:
|
||||||
return None
|
return _('Normal category')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def category_type(self):
|
def category_type(self):
|
||||||
return 'addon' if self.is_addon else 'normal'
|
return 'addon' if self.is_addon else self.cross_selling_mode or '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)
|
||||||
@@ -270,7 +308,7 @@ class SubEventItemVariation(models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_cross_sell=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
|
||||||
@@ -291,6 +329,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
|||||||
|
|
||||||
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:
|
||||||
@@ -304,8 +344,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
|||||||
|
|
||||||
|
|
||||||
class ItemQuerySet(models.QuerySet):
|
class ItemQuerySet(models.QuerySet):
|
||||||
def filter_available(self, channel='web', voucher=None, allow_addons=False):
|
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
|
||||||
return filter_available(self, channel, voucher, allow_addons)
|
return filter_available(self, channel, voucher, allow_addons, allow_cross_sell)
|
||||||
|
|
||||||
|
|
||||||
class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__):
|
class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__):
|
||||||
@@ -313,8 +353,8 @@ 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):
|
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
|
||||||
return filter_available(self.get_queryset(), channel, voucher, allow_addons)
|
return filter_available(self.get_queryset(), channel, voucher, allow_addons, allow_cross_sell)
|
||||||
|
|
||||||
|
|
||||||
class Item(LoggedModel):
|
class Item(LoggedModel):
|
||||||
@@ -1078,13 +1118,12 @@ class ItemVariation(models.Model):
|
|||||||
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
|
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
|
||||||
:type original_price: decimal.Decimal
|
:type original_price: decimal.Decimal
|
||||||
:param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after
|
:param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after
|
||||||
approval by an administrator
|
approval by an administrator
|
||||||
:type require_approval: bool
|
:type require_approval: bool
|
||||||
:param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored.
|
:param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored.
|
||||||
:type all_sales_channels: bool
|
:type all_sales_channels: bool
|
||||||
:param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on.
|
:param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on.
|
||||||
:type limit_sales_channels: list
|
:type limit_sales_channels: list
|
||||||
|
|
||||||
"""
|
"""
|
||||||
item = models.ForeignKey(
|
item = models.ForeignKey(
|
||||||
Item,
|
Item,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ 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
|
||||||
@@ -381,8 +382,28 @@ 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):
|
||||||
return hashlib.sha256(settings.SECRET_KEY.encode() + self.secret.encode()).hexdigest()[:9]
|
warnings.warn('Use email_confirm_secret() instead of email_confirm_hash().',
|
||||||
|
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
|
||||||
@@ -2835,6 +2856,14 @@ class OrderPosition(AbstractPosition):
|
|||||||
(self.order.event.settings.change_allow_user_addons and ItemAddOn.objects.filter(base_item_id__in=[op.item_id for op in positions]).exists())
|
(self.order.event.settings.change_allow_user_addons and ItemAddOn.objects.filter(base_item_id__in=[op.item_id for op in positions]).exists())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code(self):
|
||||||
|
"""
|
||||||
|
A ticket code which is unique among all events of a single organizer,
|
||||||
|
built by the order code and the position number.
|
||||||
|
"""
|
||||||
|
return '{order_code}-{position}'.format(order_code=self.order.code, position=self.positionid)
|
||||||
|
|
||||||
|
|
||||||
class Transaction(models.Model):
|
class Transaction(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -3362,6 +3391,74 @@ class BlockedTicketSecret(models.Model):
|
|||||||
unique_together = (('event', 'secret'),)
|
unique_together = (('event', 'secret'),)
|
||||||
|
|
||||||
|
|
||||||
|
class PrintLog(models.Model):
|
||||||
|
"""
|
||||||
|
A print log object is created when a ticket or badge is printed with our apps.
|
||||||
|
"""
|
||||||
|
TYPE_BADGE = 'badge'
|
||||||
|
TYPE_TICKET = 'ticket'
|
||||||
|
TYPE_CERTIFICATE = 'certificate'
|
||||||
|
TYPE_OTHER = 'other'
|
||||||
|
PRINT_TYPES = (
|
||||||
|
(TYPE_BADGE, _('Badge')),
|
||||||
|
(TYPE_TICKET, _('Ticket')),
|
||||||
|
(TYPE_CERTIFICATE, _('Certificate')),
|
||||||
|
(TYPE_OTHER, _('Other')),
|
||||||
|
)
|
||||||
|
|
||||||
|
position = models.ForeignKey(
|
||||||
|
'pretixbase.OrderPosition',
|
||||||
|
related_name='print_logs',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
successful = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Datetime of checkin, might be different from created if past scans are uploaded
|
||||||
|
datetime = models.DateTimeField(default=now)
|
||||||
|
|
||||||
|
# Datetime of creation on server
|
||||||
|
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
||||||
|
|
||||||
|
# Who printed?
|
||||||
|
device = models.ForeignKey('Device', related_name='print_logs', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
|
user = models.ForeignKey('User', related_name='print_logs', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
|
api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
|
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
# Source = Tag field with undefined values, e.g. name of app ("pretixscan")
|
||||||
|
source = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
# Type = Type of object printed ("badge", "ticket")
|
||||||
|
type = models.CharField(max_length=255, choices=PRINT_TYPES)
|
||||||
|
|
||||||
|
info = models.JSONField(default=dict)
|
||||||
|
|
||||||
|
objects = ScopedManager(organizer='position__order__event__organizer')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = (('-datetime'),)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<PrintLog: pos {} at {} from {}>".format(
|
||||||
|
self.position, self.datetime, self.source
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
super().save(**kwargs)
|
||||||
|
if self.position:
|
||||||
|
self.position.order.touch()
|
||||||
|
|
||||||
|
def delete(self, **kwargs):
|
||||||
|
super().delete(**kwargs)
|
||||||
|
self.position.order.touch()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_late_upload(self):
|
||||||
|
return self.created and abs(self.created - self.datetime) > timedelta(minutes=2)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=CachedTicket)
|
@receiver(post_delete, sender=CachedTicket)
|
||||||
def cachedticket_delete(sender, instance, **kwargs):
|
def cachedticket_delete(sender, instance, **kwargs):
|
||||||
if instance.file:
|
if instance.file:
|
||||||
|
|||||||
@@ -53,6 +53,30 @@ class SeatingPlanLayoutValidator:
|
|||||||
e = str(e).replace('%', '%%')
|
e = str(e).replace('%', '%%')
|
||||||
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(e))
|
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
seat_guids = set()
|
||||||
|
for z in val["zones"]:
|
||||||
|
for r in z["rows"]:
|
||||||
|
for s in r["seats"]:
|
||||||
|
if not s.get("seat_guid"):
|
||||||
|
raise ValidationError(
|
||||||
|
_("Seat with zone {zone}, row {row}, and number {number} has no seat ID.").format(
|
||||||
|
zone=z["name"],
|
||||||
|
row=r["row_number"],
|
||||||
|
number=s["seat_number"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif s["seat_guid"] in seat_guids:
|
||||||
|
raise ValidationError(
|
||||||
|
_("Multiple seats have the same ID: {id}").format(
|
||||||
|
id=s["seat_guid"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
seat_guids.add(s["seat_guid"])
|
||||||
|
except ValidationError as e:
|
||||||
|
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(", ".join(e.message for e in e.error_list)))
|
||||||
|
|
||||||
|
|
||||||
class SeatingPlan(LoggedModel):
|
class SeatingPlan(LoggedModel):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -304,10 +304,24 @@ 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=base_price - subtract_from_gross, gross=base_price - subtract_from_gross, tax=Decimal('0.00'),
|
net=gross,
|
||||||
rate=rate, name=self.name
|
gross=gross,
|
||||||
|
tax=Decimal('0.00'),
|
||||||
|
rate=rate,
|
||||||
|
name=self.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
if base_price_is == 'auto':
|
if base_price_is == 'auto':
|
||||||
@@ -317,19 +331,14 @@ class TaxRule(LoggedModel):
|
|||||||
base_price_is = 'net'
|
base_price_is = 'net'
|
||||||
|
|
||||||
if base_price_is == 'gross':
|
if base_price_is == 'gross':
|
||||||
if base_price >= Decimal('0.00'):
|
gross = _limit_subtract(base_price, subtract_from_gross)
|
||||||
# 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 -= subtract_from_gross
|
gross = _limit_subtract(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:
|
||||||
|
|||||||
@@ -343,11 +343,13 @@ class CartManager:
|
|||||||
err = error_messages['some_subevent_not_started']
|
err = error_messages['some_subevent_not_started']
|
||||||
cp.addons.all().delete()
|
cp.addons.all().delete()
|
||||||
cp.delete()
|
cp.delete()
|
||||||
|
continue
|
||||||
|
|
||||||
if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
|
if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
|
||||||
err = error_messages['some_subevent_ended']
|
err = error_messages['some_subevent_ended']
|
||||||
cp.addons.all().delete()
|
cp.addons.all().delete()
|
||||||
cp.delete()
|
cp.delete()
|
||||||
|
continue
|
||||||
|
|
||||||
if cp.subevent:
|
if cp.subevent:
|
||||||
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||||
@@ -360,6 +362,7 @@ class CartManager:
|
|||||||
err = error_messages['some_subevent_ended']
|
err = error_messages['some_subevent_ended']
|
||||||
cp.addons.all().delete()
|
cp.addons.all().delete()
|
||||||
cp.delete()
|
cp.delete()
|
||||||
|
continue
|
||||||
return err
|
return err
|
||||||
|
|
||||||
def _update_subevents_cache(self, se_ids: List[int]):
|
def _update_subevents_cache(self, se_ids: List[int]):
|
||||||
@@ -1542,10 +1545,9 @@ 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 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):
|
||||||
try:
|
try:
|
||||||
@@ -1566,10 +1568,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 a list of items from a user's cart.
|
Removes an item specified by its position ID 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 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):
|
||||||
try:
|
try:
|
||||||
@@ -1590,9 +1592,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 a list of items from a user's cart.
|
Removes all items from a user's cart.
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
: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):
|
||||||
try:
|
try:
|
||||||
@@ -1611,13 +1613,15 @@ 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], cart_id: str=None, locale='en',
|
def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: 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:
|
||||||
"""
|
"""
|
||||||
Removes a list of items from a user's cart.
|
Assigns addons to eligible products in a user's cart, adding and removing the addon products as necessary to
|
||||||
|
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 session: Session ID of a guest
|
:param add_to_cart_items: A list of dicts with the keys item, variation, count, custom_price, voucher, seat ID
|
||||||
|
: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
|
||||||
@@ -1635,6 +1639,7 @@ def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, 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, order_placed, periodic_task
|
from pretix.base.signals import checkin_created, periodic_task
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.jsonlogic import Logic
|
from pretix.helpers.jsonlogic import Logic
|
||||||
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
||||||
@@ -1154,23 +1154,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(order_placed, dispatch_uid="legacy_autocheckin_order_placed")
|
|
||||||
def order_placed(sender, **kwargs):
|
|
||||||
order = kwargs['order']
|
|
||||||
event = sender
|
|
||||||
|
|
||||||
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels=order.sales_channel).prefetch_related(
|
|
||||||
'limit_products'))
|
|
||||||
if not cls:
|
|
||||||
return
|
|
||||||
for op in order.positions.all():
|
|
||||||
for cl in cls:
|
|
||||||
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
|
|
||||||
if not cl.subevent_id or cl.subevent_id == op.subevent_id:
|
|
||||||
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True, type=Checkin.TYPE_ENTRY)
|
|
||||||
checkin_created.send(event, checkin=ci)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(periodic_task, dispatch_uid="autocheckout_exit_all")
|
@receiver(periodic_task, dispatch_uid="autocheckout_exit_all")
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def process_exit_all(sender, **kwargs):
|
def process_exit_all(sender, **kwargs):
|
||||||
@@ -1182,10 +1165,11 @@ def process_exit_all(sender, **kwargs):
|
|||||||
positions = cl.positions_inside_query(ignore_status=True, at_time=cl.exit_all_at)
|
positions = cl.positions_inside_query(ignore_status=True, at_time=cl.exit_all_at)
|
||||||
for p in positions:
|
for p in positions:
|
||||||
with scope(organizer=cl.event.organizer):
|
with scope(organizer=cl.event.organizer):
|
||||||
ci = Checkin.objects.create(
|
ci, created = Checkin.objects.get_or_create(
|
||||||
position=p, list=cl, auto_checked_in=True, type=Checkin.TYPE_EXIT, datetime=cl.exit_all_at
|
position=p, list=cl, auto_checked_in=True, type=Checkin.TYPE_EXIT, datetime=cl.exit_all_at
|
||||||
)
|
)
|
||||||
checkin_created.send(cl.event, checkin=ci)
|
if created:
|
||||||
|
checkin_created.send(cl.event, checkin=ci)
|
||||||
d = cl.exit_all_at.astimezone(cl.event.timezone)
|
d = cl.exit_all_at.astimezone(cl.event.timezone)
|
||||||
if cl.event.settings.get(f'autocheckin_dst_hack_{cl.pk}'): # move time back if yesterday was DST switch
|
if cl.event.settings.get(f'autocheckin_dst_hack_{cl.pk}'): # move time back if yesterday was DST switch
|
||||||
d -= timedelta(hours=1)
|
d -= timedelta(hours=1)
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
#
|
||||||
|
# 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 decimal import Decimal
|
||||||
|
from itertools import groupby
|
||||||
|
from math import inf
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
from pretix.base.models import CartPosition, ItemCategory, SalesChannel
|
||||||
|
from pretix.presale.views.event import get_grouped_items
|
||||||
|
|
||||||
|
|
||||||
|
class DummyCategory:
|
||||||
|
"""
|
||||||
|
Used to create fake category objects for displaying the same cross-selling category multiple times,
|
||||||
|
once for each subevent
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, category: ItemCategory, subevent):
|
||||||
|
self.id = category.id
|
||||||
|
self.name = str(category.name)
|
||||||
|
self.subevent_name = str(subevent)
|
||||||
|
self.description = category.description
|
||||||
|
|
||||||
|
|
||||||
|
class CrossSellingService:
|
||||||
|
def __init__(self, event, sales_channel: SalesChannel, cartpositions: List[CartPosition], customer):
|
||||||
|
self.event = event
|
||||||
|
self.sales_channel = sales_channel
|
||||||
|
self.cartpositions = cartpositions
|
||||||
|
self.customer = customer
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
if self.event.has_subevents:
|
||||||
|
subevents = set(pos.subevent for pos in self.cartpositions)
|
||||||
|
result = (
|
||||||
|
(DummyCategory(category, subevent),
|
||||||
|
self._prepare_items(subevent, items_qs, discount_info),
|
||||||
|
f'subevent_{subevent.pk}_')
|
||||||
|
for subevent in subevents
|
||||||
|
for (category, items_qs, discount_info) in self._applicable_categories(subevent.pk)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = (
|
||||||
|
(category,
|
||||||
|
self._prepare_items(None, items_qs, discount_info),
|
||||||
|
'')
|
||||||
|
for (category, items_qs, discount_info) in self._applicable_categories(0)
|
||||||
|
)
|
||||||
|
result = [(category, items, form_prefix) for (category, items, form_prefix) in result if len(items) > 0]
|
||||||
|
for category, items, form_prefix in result:
|
||||||
|
category.category_has_discount = any(item.original_price or (
|
||||||
|
item.has_variations and any(var.original_price for var in item.available_variations)
|
||||||
|
) for item in items)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _applicable_categories(self, subevent_id):
|
||||||
|
return [
|
||||||
|
(c, products_qs, discount_info) for (c, products_qs, discount_info) in
|
||||||
|
(
|
||||||
|
(c, *self._get_visible_items_for_category(subevent_id, c))
|
||||||
|
for c in self.event.categories.filter(cross_selling_mode__isnull=False).prefetch_related('items')
|
||||||
|
)
|
||||||
|
if products_qs is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
def _get_visible_items_for_category(self, filter_subevent_id, category: ItemCategory):
|
||||||
|
"""
|
||||||
|
If this category should be visible in the cross-selling step for a given cart and sales_channel, this method
|
||||||
|
returns a queryset of the items that should be displayed, as well as a dict giving additional information on them.
|
||||||
|
|
||||||
|
:returns: (QuerySet<Item>, dict<(subevent_id, item_pk): (max_count, discount_rule)>)
|
||||||
|
max_count is `inf` if the item should not be limited
|
||||||
|
discount_rule is None if the item will not be discounted
|
||||||
|
"""
|
||||||
|
if category.cross_selling_mode is None:
|
||||||
|
return None, {}
|
||||||
|
if category.cross_selling_condition == 'always':
|
||||||
|
return category.items.all(), {}
|
||||||
|
if category.cross_selling_condition == 'products':
|
||||||
|
match = set(match.pk for match in category.cross_selling_match_products.only('pk')) # TODO prefetch this
|
||||||
|
return (category.items.all(), {}) if any(pos.item.pk in match for pos in self.cartpositions) else (None, {})
|
||||||
|
if category.cross_selling_condition == 'discounts':
|
||||||
|
my_item_pks = [item.id for item in category.items.all()]
|
||||||
|
potential_discount_items = {
|
||||||
|
item.pk: (max_count, discount_rule)
|
||||||
|
for subevent_id, item, max_count, discount_rule in self._potential_discounts_by_subevent_and_item_for_current_cart
|
||||||
|
if max_count > 0 and item.pk in my_item_pks and item.is_available() and (subevent_id == filter_subevent_id or subevent_id is None)
|
||||||
|
}
|
||||||
|
return category.items.filter(pk__in=potential_discount_items), potential_discount_items
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def _potential_discounts_by_subevent_and_item_for_current_cart(self):
|
||||||
|
potential_discounts_by_cartpos = defaultdict(list)
|
||||||
|
|
||||||
|
from ..services.pricing import apply_discounts
|
||||||
|
self._discounted_prices = apply_discounts(
|
||||||
|
self.event,
|
||||||
|
self.sales_channel,
|
||||||
|
[
|
||||||
|
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled,
|
||||||
|
cp.listed_price - cp.price_after_voucher)
|
||||||
|
for cp in self.cartpositions
|
||||||
|
],
|
||||||
|
collect_potential_discounts=potential_discounts_by_cartpos
|
||||||
|
)
|
||||||
|
|
||||||
|
# flatten potential_discounts_by_cartpos (a dict of lists of potential discounts) into a set of potential discounts
|
||||||
|
# (which is technically stored as a dict, but we use it as an OrderedSet here)
|
||||||
|
potential_discount_set = dict.fromkeys(
|
||||||
|
info for lst in potential_discounts_by_cartpos.values() for info in lst)
|
||||||
|
|
||||||
|
# sum up the max_counts and pass them on (also pass on the discount_rules so we can calculate actual discounted prices later):
|
||||||
|
# group by benefit product
|
||||||
|
# - max_count for product: sum up max_counts
|
||||||
|
# - discount_rule for product: take first discount_rule
|
||||||
|
|
||||||
|
def discount_info(subevent_id, item, infos_for_item):
|
||||||
|
infos_for_item = list(infos_for_item)
|
||||||
|
return (
|
||||||
|
subevent_id,
|
||||||
|
item,
|
||||||
|
sum(max_count for (subevent_id, item, discount_rule, max_count, i) in infos_for_item),
|
||||||
|
next(discount_rule for (subevent_id, item, discount_rule, max_count, i) in infos_for_item),
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
discount_info(subevent_id, item, infos_for_item) for (subevent_id, item), infos_for_item in
|
||||||
|
groupby(
|
||||||
|
sorted(
|
||||||
|
(
|
||||||
|
(subevent_id, item, discount_rule, max_count, i)
|
||||||
|
for (discount_rule, max_count, i, subevent_id) in potential_discount_set.keys()
|
||||||
|
for item in discount_rule.benefit_limit_products.all()
|
||||||
|
),
|
||||||
|
key=lambda tup: (tup[0], tup[1].pk)
|
||||||
|
),
|
||||||
|
lambda tup: (tup[0], tup[1]))
|
||||||
|
]
|
||||||
|
|
||||||
|
def _prepare_items(self, subevent, items_qs, discount_info):
|
||||||
|
items, _btn = get_grouped_items(
|
||||||
|
self.event,
|
||||||
|
subevent=subevent,
|
||||||
|
voucher=None,
|
||||||
|
channel=self.sales_channel,
|
||||||
|
base_qs=items_qs,
|
||||||
|
allow_addons=False,
|
||||||
|
allow_cross_sell=True,
|
||||||
|
memberships=(
|
||||||
|
self.customer.usable_memberships(
|
||||||
|
for_event=subevent or self.event,
|
||||||
|
testmode=self.event.testmode
|
||||||
|
)
|
||||||
|
if self.customer else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
new_items = list()
|
||||||
|
for item in items:
|
||||||
|
max_count = inf
|
||||||
|
if item.pk in discount_info:
|
||||||
|
(max_count, discount_rule) = discount_info[item.pk]
|
||||||
|
|
||||||
|
# only benefit_only_apply_to_cheapest_n_matches discounted items have a max_count, all others get 'inf'
|
||||||
|
if not max_count:
|
||||||
|
max_count = inf
|
||||||
|
|
||||||
|
# calculate discounted price
|
||||||
|
if discount_rule and discount_rule.benefit_discount_matching_percent > 0:
|
||||||
|
if not item.has_variations:
|
||||||
|
item.original_price = item.original_price or item.display_price
|
||||||
|
previous_price = item.display_price
|
||||||
|
new_price = (
|
||||||
|
previous_price * (
|
||||||
|
(Decimal('100.00') - discount_rule.benefit_discount_matching_percent) / Decimal('100.00'))
|
||||||
|
)
|
||||||
|
item.display_price = new_price
|
||||||
|
else:
|
||||||
|
# discounts always match "whole" items, not specific variations -> we apply the discount to all
|
||||||
|
# available variations of the item
|
||||||
|
for var in item.available_variations:
|
||||||
|
var.original_price = var.original_price or var.display_price
|
||||||
|
previous_price = var.display_price
|
||||||
|
new_price = (
|
||||||
|
previous_price * (
|
||||||
|
(Decimal('100.00') - discount_rule.benefit_discount_matching_percent) / Decimal('100.00'))
|
||||||
|
)
|
||||||
|
var.display_price = new_price
|
||||||
|
|
||||||
|
if not item.has_variations:
|
||||||
|
# reduce order_max by number of items already in cart (prevent recommending a product the user can't add anyway)
|
||||||
|
item.order_max = min(
|
||||||
|
item.order_max - sum(1 for pos in self.cartpositions if pos.item_id == item.pk),
|
||||||
|
max_count
|
||||||
|
)
|
||||||
|
if item.order_max > 0:
|
||||||
|
new_items.append(item)
|
||||||
|
else:
|
||||||
|
new_vars = list()
|
||||||
|
for var in item.available_variations:
|
||||||
|
# reduce order_max by number of items already in cart (prevent recommending a product the user can't add anyway)
|
||||||
|
var.order_max = min(
|
||||||
|
var.order_max - sum(1 for pos in self.cartpositions if pos.item_id == item.pk and pos.variation_id == var.pk),
|
||||||
|
max_count
|
||||||
|
)
|
||||||
|
if var.order_max > 0:
|
||||||
|
new_vars.append(var)
|
||||||
|
if len(new_vars):
|
||||||
|
item.available_variations = new_vars
|
||||||
|
new_items.append(item)
|
||||||
|
|
||||||
|
return new_items
|
||||||
@@ -301,7 +301,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
order.event, 'presale:event.order.open', kwargs={
|
order.event, 'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
'hash': order.email_confirm_hash()
|
'hash': order.email_confirm_secret()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'presale:event.order.open', kwargs={
|
'presale:event.order.open', kwargs={
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
'hash': order.email_confirm_hash()
|
'hash': order.email_confirm_secret()
|
||||||
}
|
}
|
||||||
), lambda event: build_absolute_uri(
|
), lambda event: build_absolute_uri(
|
||||||
event,
|
event,
|
||||||
@@ -443,7 +443,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'organizer': event.organizer.slug,
|
'organizer': event.organizer.slug,
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'secret': order.secret,
|
'secret': order.secret,
|
||||||
'hash': order.email_confirm_hash(),
|
'hash': order.email_confirm_secret(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
for order in orders
|
for order in orders
|
||||||
|
|||||||
@@ -20,8 +20,9 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import re
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple, Union
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@@ -31,6 +32,7 @@ from pretix.base.models import (
|
|||||||
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation,
|
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation,
|
||||||
SalesChannel, Voucher,
|
SalesChannel, Voucher,
|
||||||
)
|
)
|
||||||
|
from pretix.base.models.discount import Discount, PositionInfo
|
||||||
from pretix.base.models.event import Event, SubEvent
|
from pretix.base.models.event import Event, SubEvent
|
||||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||||
from pretix.base.timemachine import time_machine_now
|
from pretix.base.timemachine import time_machine_now
|
||||||
@@ -155,14 +157,22 @@ def get_line_price(price_after_voucher: Decimal, custom_price_input: Decimal, cu
|
|||||||
return price
|
return price
|
||||||
|
|
||||||
|
|
||||||
def apply_discounts(event: Event, sales_channel: str,
|
def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
|
||||||
positions: List[Tuple[int, Optional[int], Decimal, bool, bool]]) -> List[Decimal]:
|
positions: List[Tuple[int, Optional[int], Decimal, bool, bool, Decimal]],
|
||||||
|
collect_potential_discounts: Optional[defaultdict]=None) -> List[Tuple[Decimal, Optional[Discount]]]:
|
||||||
"""
|
"""
|
||||||
Applies any dynamic discounts to a cart
|
Applies any dynamic discounts to a cart
|
||||||
|
|
||||||
:param event: Event the cart belongs to
|
:param event: Event the cart belongs to
|
||||||
:param sales_channel: Sales channel the cart was created with
|
:param sales_channel: Sales channel the cart was created with
|
||||||
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
|
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
|
||||||
|
:param collect_potential_discounts: If a `defaultdict(list)` is supplied, all discounts that could be applied to the cart
|
||||||
|
based on the "consumed" items, but lack matching "benefitting" items will be collected therein.
|
||||||
|
The dict will contain a mapping from index in the `positions` list of the item that could be consumed, to a list
|
||||||
|
of tuples describing the discounts that could be applied in the form `(discount, max_count, grouping_id)`.
|
||||||
|
`max_count` is either the maximum number of benefitting items that the discount would apply to, or `inf` if that number
|
||||||
|
is not limited. The `grouping_id` can be used to distinguish several occurrences of the same discount.
|
||||||
|
|
||||||
:return: A list of ``(new_gross_price, discount)`` tuples in the same order as the input
|
:return: A list of ``(new_gross_price, discount)`` tuples in the same order as the input
|
||||||
"""
|
"""
|
||||||
if isinstance(sales_channel, SalesChannel):
|
if isinstance(sales_channel, SalesChannel):
|
||||||
@@ -177,10 +187,10 @@ def apply_discounts(event: Event, sales_channel: str,
|
|||||||
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
|
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
|
||||||
for discount in discount_qs:
|
for discount in discount_qs:
|
||||||
result = discount.apply({
|
result = discount.apply({
|
||||||
idx: (item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)
|
idx: PositionInfo(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)
|
||||||
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount) in enumerate(positions)
|
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount) in enumerate(positions)
|
||||||
if not is_bundled and idx not in new_prices
|
if not is_bundled and idx not in new_prices
|
||||||
})
|
}, collect_potential_discounts)
|
||||||
for k in result.keys():
|
for k in result.keys():
|
||||||
result[k] = (result[k], discount)
|
result[k] = (result[k], discount)
|
||||||
new_prices.update(result)
|
new_prices.update(result)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
|||||||
v.tag = r.get('tag')
|
v.tag = r.get('tag')
|
||||||
if v.comment:
|
if v.comment:
|
||||||
v.comment += '\n\n'
|
v.comment += '\n\n'
|
||||||
v.comment = gettext('The voucher has been sent to {recipient}.').format(recipient=r['email'])
|
v.comment += gettext('The voucher has been sent to {recipient}.').format(recipient=r['email'])
|
||||||
logs.append(v.log_action(
|
logs.append(v.log_action(
|
||||||
'pretix.voucher.sent',
|
'pretix.voucher.sent',
|
||||||
user=user,
|
user=user,
|
||||||
|
|||||||
@@ -571,7 +571,7 @@ DEFAULTS = {
|
|||||||
'form_class': I18nFormField,
|
'form_class': I18nFormField,
|
||||||
'serializer_class': I18nField,
|
'serializer_class': I18nField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Custom recipient field"),
|
label=_("Custom recipient field label"),
|
||||||
widget=I18nTextInput,
|
widget=I18nTextInput,
|
||||||
help_text=_("If you want to add a custom text field, e.g. for a country-specific registration number, to "
|
help_text=_("If you want to add a custom text field, e.g. for a country-specific registration number, to "
|
||||||
"your invoice address form, please fill in the label here. This label will both be used for "
|
"your invoice address form, please fill in the label here. This label will both be used for "
|
||||||
@@ -580,6 +580,18 @@ DEFAULTS = {
|
|||||||
"The field will not be required.")
|
"The field will not be required.")
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'invoice_address_custom_field_helptext': {
|
||||||
|
'default': '',
|
||||||
|
'type': LazyI18nString,
|
||||||
|
'form_class': I18nFormField,
|
||||||
|
'serializer_class': I18nField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Custom recipient field help text"),
|
||||||
|
widget=I18nTextInput,
|
||||||
|
help_text=_("If you use the custom recipient field, you can specify a help text which will be displayed "
|
||||||
|
"underneath the field. It will not be displayed on the invoice.")
|
||||||
|
)
|
||||||
|
},
|
||||||
'invoice_address_vatid': {
|
'invoice_address_vatid': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ validate_cart_addons = EventPluginSignal()
|
|||||||
Arguments: ``addons``, ``base_position``, ``iao``
|
Arguments: ``addons``, ``base_position``, ``iao``
|
||||||
|
|
||||||
This signal is sent when a user tries to select a combination of addons. In contrast to
|
This signal is sent when a user tries to select a combination of addons. In contrast to
|
||||||
``validate_cart``, this is executed before the cart is actually modified. You are passed
|
``validate_cart``, this is executed before the cart is actually modified. You are passed
|
||||||
an argument ``addons`` containing a dict of ``(item, variation or None) → count`` tuples as well
|
an argument ``addons`` containing a dict of ``(item, variation or None) → count`` tuples as well
|
||||||
as the ``ItemAddOn`` object as the argument ``iao`` and the base cart position as
|
as the ``ItemAddOn`` object as the argument ``iao`` and the base cart position as
|
||||||
``base_position``.
|
``base_position``.
|
||||||
@@ -838,3 +838,12 @@ is given as the first argument.
|
|||||||
|
|
||||||
The ``sender`` keyword argument will contain the organizer.
|
The ``sender`` keyword argument will contain the organizer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
device_info_updated = django.dispatch.Signal()
|
||||||
|
"""
|
||||||
|
Arguments: ``old_device``, ``new_device``
|
||||||
|
|
||||||
|
This signal is sent out each time the information for a Device is modified.
|
||||||
|
Both the original and updated versions of the Device are included to allow
|
||||||
|
receivers to see what has been updated.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -143,7 +143,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<div class="order-button">
|
<div class="order-button">
|
||||||
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}" class="button">
|
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_secret order=order.code secret=order.secret %}" class="button">
|
||||||
{% trans "View order details" %}
|
{% trans "View order details" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ def timeline_for_event(event, subevent=None):
|
|||||||
tl.append(TimelineEvent(
|
tl.append(TimelineEvent(
|
||||||
event=event, subevent=subevent,
|
event=event, subevent=subevent,
|
||||||
datetime=rd.datetime(ev),
|
datetime=rd.datetime(ev),
|
||||||
description=pgettext_lazy('timeline', 'Customers can no longer modify their orders'),
|
description=pgettext_lazy('timeline', 'Customers can no longer modify their order information'),
|
||||||
edit_url=ev_edit_url
|
edit_url=ev_edit_url
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -159,6 +159,18 @@ def timeline_for_event(event, subevent=None):
|
|||||||
})
|
})
|
||||||
))
|
))
|
||||||
|
|
||||||
|
rd = event.settings.get('change_allow_user_until', as_type=RelativeDateWrapper)
|
||||||
|
if rd and event.settings.change_allow_user_until:
|
||||||
|
tl.append(TimelineEvent(
|
||||||
|
event=event, subevent=subevent,
|
||||||
|
datetime=rd.datetime(ev),
|
||||||
|
description=pgettext_lazy('timeline', 'Customers can no longer make changes to their orders'),
|
||||||
|
edit_url=reverse('control:event.settings.cancel', kwargs={
|
||||||
|
'event': event.slug,
|
||||||
|
'organizer': event.organizer.slug
|
||||||
|
})
|
||||||
|
))
|
||||||
|
|
||||||
rd = event.settings.get('waiting_list_auto_disable', as_type=RelativeDateWrapper)
|
rd = event.settings.get('waiting_list_auto_disable', as_type=RelativeDateWrapper)
|
||||||
if rd and event.settings.waiting_list_enabled:
|
if rd and event.settings.waiting_list_enabled:
|
||||||
tl.append(TimelineEvent(
|
tl.append(TimelineEvent(
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import sys
|
|||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.urls import Resolver404, get_script_prefix, resolve
|
from django.urls import Resolver404, get_script_prefix, resolve
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
@@ -152,6 +153,8 @@ def _default_context(request):
|
|||||||
ctx['warning_update_available'] = True
|
ctx['warning_update_available'] = True
|
||||||
if not gs.settings.update_check_ack and 'runserver' not in sys.argv:
|
if not gs.settings.update_check_ack and 'runserver' not in sys.argv:
|
||||||
ctx['warning_update_check_active'] = True
|
ctx['warning_update_check_active'] = True
|
||||||
|
if not cache.get('pretix_runperiodic_executed') and not settings.DEBUG:
|
||||||
|
ctx['warning_cronjob'] = True
|
||||||
|
|
||||||
ctx['ie_deprecation_warning'] = 'MSIE' in request.headers.get('User-Agent', '') or 'Trident/' in request.headers.get('User-Agent', '')
|
ctx['ie_deprecation_warning'] = 'MSIE' in request.headers.get('User-Agent', '') or 'Trident/' in request.headers.get('User-Agent', '')
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ from django_scopes.forms import (
|
|||||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||||
from pretix.base.models import Gate
|
from pretix.base.models import Gate
|
||||||
from pretix.base.models.checkin import Checkin, CheckinList
|
from pretix.base.models.checkin import Checkin, CheckinList
|
||||||
from pretix.control.forms import (
|
from pretix.control.forms import ItemMultipleChoiceField
|
||||||
ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
|
|
||||||
)
|
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
|
|
||||||
|
|
||||||
@@ -67,10 +65,6 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
kwargs.pop('locales', None)
|
kwargs.pop('locales', None)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.fields['limit_products'].queryset = self.event.items.all()
|
self.fields['limit_products'].queryset = self.event.items.all()
|
||||||
self.fields['auto_checkin_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
|
||||||
self.fields['auto_checkin_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(
|
|
||||||
self.event, choices=self.fields['auto_checkin_sales_channels'].widget.choices
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.event.organizer.gates.exists():
|
if not self.event.organizer.gates.exists():
|
||||||
del self.fields['gates']
|
del self.fields['gates']
|
||||||
@@ -102,7 +96,6 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
'limit_products',
|
'limit_products',
|
||||||
'subevent',
|
'subevent',
|
||||||
'include_pending',
|
'include_pending',
|
||||||
'auto_checkin_sales_channels',
|
|
||||||
'allow_multiple_entries',
|
'allow_multiple_entries',
|
||||||
'allow_entry_after_exit',
|
'allow_entry_after_exit',
|
||||||
'rules',
|
'rules',
|
||||||
@@ -125,7 +118,6 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
'limit_products': ItemMultipleChoiceField,
|
'limit_products': ItemMultipleChoiceField,
|
||||||
'gates': SafeModelMultipleChoiceField,
|
'gates': SafeModelMultipleChoiceField,
|
||||||
'subevent': SafeModelChoiceField,
|
'subevent': SafeModelChoiceField,
|
||||||
'auto_checkin_sales_channels': SafeModelMultipleChoiceField,
|
|
||||||
'exit_all_at': NextTimeField,
|
'exit_all_at': NextTimeField,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -844,6 +844,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
|||||||
'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_include_free',
|
'invoice_include_free',
|
||||||
|
|||||||
@@ -1967,7 +1967,7 @@ class CheckinListAttendeeFilterForm(FilterForm):
|
|||||||
if s == '1':
|
if s == '1':
|
||||||
qs = qs.filter(last_entry__isnull=False)
|
qs = qs.filter(last_entry__isnull=False)
|
||||||
elif s == '2':
|
elif s == '2':
|
||||||
qs = qs.filter(pk__in=self.list.positions_inside.values_list('pk'))
|
qs = self.list._filter_positions_inside(qs)
|
||||||
elif s == '3':
|
elif s == '3':
|
||||||
qs = qs.filter(last_entry__isnull=False).filter(
|
qs = qs.filter(last_entry__isnull=False).filter(
|
||||||
Q(last_exit__isnull=False) & Q(last_exit__gte=F('last_entry'))
|
Q(last_exit__isnull=False) & Q(last_exit__gte=F('last_entry'))
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ from urllib.parse import urlencode
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Max
|
from django.db.models import Max, Q
|
||||||
|
from django.forms import ChoiceField, RadioSelect
|
||||||
from django.forms.formsets import DELETION_FIELD_NAME
|
from django.forms.formsets import DELETION_FIELD_NAME
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -79,11 +80,67 @@ class CategoryForm(I18nModelForm):
|
|||||||
'name',
|
'name',
|
||||||
'internal_name',
|
'internal_name',
|
||||||
'description',
|
'description',
|
||||||
'is_addon'
|
'cross_selling_condition',
|
||||||
|
'cross_selling_match_products',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'description': I18nMarkdownTextarea,
|
'description': I18nMarkdownTextarea,
|
||||||
|
'cross_selling_condition': RadioSelect,
|
||||||
}
|
}
|
||||||
|
field_classes = {
|
||||||
|
'cross_selling_match_products': SafeModelMultipleChoiceField,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
tpl = '{} <span class="text-muted">{}</span>'
|
||||||
|
self.fields['category_type'] = ChoiceField(widget=RadioSelect, choices=(
|
||||||
|
('normal', mark_safe(tpl.format(
|
||||||
|
_('Normal category'),
|
||||||
|
_('Products in this category are regular products displayed on the front page.')
|
||||||
|
)),),
|
||||||
|
('addon', mark_safe(tpl.format(
|
||||||
|
_('Add-on product category'),
|
||||||
|
_('Products in this category are add-on products and can only be bought as add-ons.')
|
||||||
|
)),),
|
||||||
|
('only', mark_safe(tpl.format(
|
||||||
|
_('Cross-selling category'),
|
||||||
|
_('Products in this category are regular products, but are only shown in the cross-selling step, '
|
||||||
|
'according to the configuration below.')
|
||||||
|
)),),
|
||||||
|
('both', mark_safe(tpl.format(
|
||||||
|
_('Normal + cross-selling category'),
|
||||||
|
_('Products in this category are regular products displayed on the front page, but are additionally '
|
||||||
|
'shown in the cross-selling step, according to the configuration below.')
|
||||||
|
)),),
|
||||||
|
))
|
||||||
|
self.fields['category_type'].initial = self.instance.category_type
|
||||||
|
|
||||||
|
self.fields['cross_selling_condition'].widget.attrs['data-display-dependency'] = '#id_category_type_2,#id_category_type_3'
|
||||||
|
self.fields['cross_selling_condition'].widget.attrs['data-disable-dependent'] = 'true'
|
||||||
|
self.fields['cross_selling_condition'].widget.choices = self.fields['cross_selling_condition'].widget.choices[1:]
|
||||||
|
self.fields['cross_selling_condition'].required = False
|
||||||
|
|
||||||
|
self.fields['cross_selling_match_products'].widget = forms.CheckboxSelectMultiple(
|
||||||
|
attrs={
|
||||||
|
'class': 'scrolling-multiple-choice',
|
||||||
|
'data-display-dependency': '#id_cross_selling_condition_2'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.fields['cross_selling_match_products'].queryset = self.event.items.filter(
|
||||||
|
# don't show products which are only visible in addon/cross-sell step themselves
|
||||||
|
Q(category__isnull=True) | Q(
|
||||||
|
Q(category__is_addon=False) & Q(Q(category__cross_selling_mode='both') | Q(category__cross_selling_mode__isnull=True))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
d = super().clean()
|
||||||
|
if d.get('category_type') == 'only' or d.get('category_type') == 'both':
|
||||||
|
if not d.get('cross_selling_condition'):
|
||||||
|
raise ValidationError({'cross_selling_condition': [_('This field is required')]})
|
||||||
|
self.instance.category_type = d.get('category_type')
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
class QuestionForm(I18nModelForm):
|
class QuestionForm(I18nModelForm):
|
||||||
|
|||||||
@@ -609,6 +609,49 @@ class OrderFeeChangeForm(forms.Form):
|
|||||||
change_decimal_field(self.fields['value'], instance.order.event.currency)
|
change_decimal_field(self.fields['value'], instance.order.event.currency)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderFeeAddForm(forms.Form):
|
||||||
|
fee_type = forms.ChoiceField(choices=OrderFee.FEE_TYPES)
|
||||||
|
value = forms.DecimalField(
|
||||||
|
max_digits=13, decimal_places=2,
|
||||||
|
localize=True,
|
||||||
|
label=_('Price'),
|
||||||
|
help_text=_("including all taxes"),
|
||||||
|
)
|
||||||
|
tax_rule = forms.ModelChoiceField(
|
||||||
|
TaxRule.objects.none(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
description = forms.CharField(required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
order = kwargs.pop('order')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['tax_rule'].queryset = order.event.tax_rules.all()
|
||||||
|
change_decimal_field(self.fields['value'], order.event.currency)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderFeeAddFormset(forms.BaseFormSet):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.order = kwargs.pop('order', None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _construct_form(self, i, **kwargs):
|
||||||
|
kwargs['order'] = self.order
|
||||||
|
return super()._construct_form(i, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def empty_form(self):
|
||||||
|
form = self.form(
|
||||||
|
auto_id=self.auto_id,
|
||||||
|
prefix=self.add_prefix('__prefix__'),
|
||||||
|
empty_permitted=True,
|
||||||
|
use_required_attribute=False,
|
||||||
|
order=self.order,
|
||||||
|
)
|
||||||
|
self.add_fields(form, None)
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
class OrderContactForm(forms.ModelForm):
|
class OrderContactForm(forms.ModelForm):
|
||||||
regenerate_secrets = forms.BooleanField(required=False, label=_('Invalidate secrets'),
|
regenerate_secrets = forms.BooleanField(required=False, label=_('Invalidate secrets'),
|
||||||
help_text=_('Regenerates the order and ticket secrets. You will '
|
help_text=_('Regenerates the order and ticket secrets. You will '
|
||||||
|
|||||||
@@ -239,11 +239,14 @@ class VoucherForm(I18nModelForm):
|
|||||||
self.instance.event, self.instance.quota, self.instance.item, self.instance.variation
|
self.instance.event, self.instance.quota, self.instance.item, self.instance.variation
|
||||||
)
|
)
|
||||||
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
|
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
|
||||||
if 'seat' in self.fields and data.get('seat'):
|
if 'seat' in self.fields:
|
||||||
self.instance.seat = Voucher.clean_seat_id(
|
if data.get('seat'):
|
||||||
data, self.instance.item, self.instance.quota, self.instance.event, self.instance.pk
|
self.instance.seat = Voucher.clean_seat_id(
|
||||||
)
|
data, self.instance.item, self.instance.quota, self.instance.event, self.instance.pk
|
||||||
self.instance.item = self.instance.seat.product
|
)
|
||||||
|
self.instance.item = self.instance.seat.product
|
||||||
|
else:
|
||||||
|
self.instance.seat = None
|
||||||
|
|
||||||
voucher_form_validation.send(sender=self.instance.event, form=self, data=data)
|
voucher_form_validation.send(sender=self.instance.event, form=self, data=data)
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ from pretix.base.models import (
|
|||||||
Checkin, CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
|
Checkin, CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
|
||||||
TaxRule,
|
TaxRule,
|
||||||
)
|
)
|
||||||
|
from pretix.base.models.orders import PrintLog
|
||||||
from pretix.base.signals import logentry_display, orderposition_blocked_display
|
from pretix.base.signals import logentry_display, orderposition_blocked_display
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
|
|
||||||
@@ -639,6 +640,16 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
|||||||
if sender and logentry.action_type.startswith('pretix.event.checkin'):
|
if sender and logentry.action_type.startswith('pretix.event.checkin'):
|
||||||
return _display_checkin(sender, logentry)
|
return _display_checkin(sender, logentry)
|
||||||
|
|
||||||
|
if logentry.action_type == 'pretix.event.order.print':
|
||||||
|
return _('Position #{posid} has been printed at {datetime} with type "{type}".').format(
|
||||||
|
posid=data.get('positionid'),
|
||||||
|
datetime=date_format(
|
||||||
|
dateutil.parser.parse(data["datetime"]).astimezone(sender.timezone),
|
||||||
|
"SHORT_DATETIME_FORMAT"
|
||||||
|
),
|
||||||
|
type=dict(PrintLog.PRINT_TYPES)[data["type"]],
|
||||||
|
)
|
||||||
|
|
||||||
if logentry.action_type == 'pretix.control.views.checkin':
|
if logentry.action_type == 'pretix.control.views.checkin':
|
||||||
# deprecated
|
# deprecated
|
||||||
dt = dateutil.parser.parse(data.get('datetime'))
|
dt = dateutil.parser.parse(data.get('datetime'))
|
||||||
|
|||||||
@@ -421,6 +421,14 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if warning_cronjob %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
The cronjob component of pretix was not executed in the last hours. Please check that
|
||||||
|
you have completed all installation steps and your cronjob is executed correctly.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if debug_warning %}
|
{% if debug_warning %}
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
|
|||||||
@@ -67,7 +67,6 @@
|
|||||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||||
{% bootstrap_field form.addon_match layout="control" %}
|
{% bootstrap_field form.addon_match layout="control" %}
|
||||||
{% bootstrap_field form.exit_all_at layout="control" %}
|
{% bootstrap_field form.exit_all_at layout="control" %}
|
||||||
{% bootstrap_field form.auto_checkin_sales_channels layout="control" %}
|
|
||||||
{% if form.gates %}
|
{% if form.gates %}
|
||||||
{% bootstrap_field form.gates layout="control" %}
|
{% bootstrap_field form.gates layout="control" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -101,7 +101,6 @@
|
|||||||
<a href="?{% url_replace request 'ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a>
|
<a href="?{% url_replace request 'ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a>
|
||||||
</th>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th class="iconcol">{% trans "Automated check-in" %}</th>
|
|
||||||
<th>{% trans "Products" %}</th>
|
<th>{% trans "Products" %}</th>
|
||||||
<th class="action-col-2"></th>
|
<th class="action-col-2"></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -137,17 +136,6 @@
|
|||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
|
||||||
{% for channel in cl.auto_checkin_sales_channels.all %}
|
|
||||||
{% if "." in channel.icon %}
|
|
||||||
<img src="{% static channel.icon %}" class="fa-like-image"
|
|
||||||
data-toggle="tooltip" title="{{ channel.label }}">
|
|
||||||
{% else %}
|
|
||||||
<span class="fa fa-{{ channel.icon }} text-muted"
|
|
||||||
data-toggle="tooltip" title="{{ channel.label }}"></span>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
{% if cl.all_products %}
|
{% if cl.all_products %}
|
||||||
<em>{% trans "All" %}</em>
|
<em>{% trans "All" %}</em>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
{% bootstrap_field form.invoice_address_beneficiary layout="control" %}
|
{% bootstrap_field form.invoice_address_beneficiary layout="control" %}
|
||||||
{% bootstrap_field form.invoice_address_not_asked_free layout="control" %}
|
{% bootstrap_field form.invoice_address_not_asked_free layout="control" %}
|
||||||
{% bootstrap_field form.invoice_address_custom_field layout="control" %}
|
{% bootstrap_field form.invoice_address_custom_field layout="control" %}
|
||||||
|
{% bootstrap_field form.invoice_address_custom_field_helptext layout="control" %}
|
||||||
{% bootstrap_field form.invoice_address_explanation_text layout="control" %}
|
{% bootstrap_field form.invoice_address_explanation_text layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
{% bootstrap_field form.eu_reverse_charge layout="control" %}
|
{% bootstrap_field form.eu_reverse_charge layout="control" %}
|
||||||
{% bootstrap_field form.home_country layout="control" %}
|
{% bootstrap_field form.home_country layout="control" %}
|
||||||
{% bootstrap_field form.keep_gross_if_rate_changes layout="control" %}
|
{% bootstrap_field form.keep_gross_if_rate_changes layout="control" %}
|
||||||
<h3>{% trans "Custom taxation rules" %}</h3>
|
<h3>{% trans "Custom rules" %}</h3>
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
These settings are intended for professional users with very specific taxation situations.
|
These settings are intended for professional users with very specific taxation situations.
|
||||||
|
|||||||
@@ -12,10 +12,22 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if object.id and not object.quotas.exists %}
|
{% if object.id and not object.quotas.exists %}
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
{% blocktrans trimmed %}
|
<div class="row">
|
||||||
Please note that your product will <strong>not</strong> be available for sale until you have added your
|
<div class="col-lg-8">
|
||||||
item to an existing or newly created quota.
|
{% blocktrans trimmed %}
|
||||||
{% endblocktrans %}
|
Please note that your product will <strong>not</strong> be available for sale until you have added your
|
||||||
|
item to an existing or newly created quota.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 text-right">
|
||||||
|
<a class="btn btn-default btn-sm" href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||||
|
<i class="fa fa-wrench"></i> {% trans "Manage quotas" %}
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-default btn-sm" href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?{% if object.has_variations %}{% for var in object.variations.all %}product={{ object.pk }}-{{ var.pk }}&{% endfor %}{% else %}product={{ object.pk }}{% endif %}">
|
||||||
|
<i class="fa fa-plus"></i> {% trans "Create a new quota" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% elif object.pk and not object.is_available_by_time %}
|
{% elif object.pk and not object.is_available_by_time %}
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
|
|||||||
@@ -67,10 +67,22 @@
|
|||||||
<div class="panel-body form-horizontal">
|
<div class="panel-body form-horizontal">
|
||||||
{% if form.instance.pk and not form.instance.quotas.exists %}
|
{% if form.instance.pk and not form.instance.quotas.exists %}
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
{% blocktrans trimmed %}
|
<div class="row">
|
||||||
Please note that your variation will <strong>not</strong> be available for sale
|
<div class="col-lg-8">
|
||||||
until you have added it to an existing or newly created quota.
|
{% blocktrans trimmed %}
|
||||||
{% endblocktrans %}
|
Please note that your variation will <strong>not</strong> be available for sale
|
||||||
|
until you have added it to an existing or newly created quota.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 text-right">
|
||||||
|
<a class="btn btn-default btn-xs" href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||||
|
<i class="fa fa-wrench"></i> {% trans "Manage quotas" %}
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-default btn-xs" href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?product={{ form.instance.item.pk }}-{{ form.instance.pk }}">
|
||||||
|
<i class="fa fa-plus"></i> {% trans "Create a new quota" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_form_errors form %}
|
{% bootstrap_form_errors form %}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Product categories" %}</th>
|
<th>{% trans "Product categories" %}</th>
|
||||||
|
<th>{% trans "Category type" %}</th>
|
||||||
<th class="action-col-2"></th>
|
<th class="action-col-2"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -40,6 +41,9 @@
|
|||||||
<td>
|
<td>
|
||||||
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
|
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ c.get_category_type_display }}
|
||||||
|
</td>
|
||||||
<td class="text-right flip">
|
<td class="text-right flip">
|
||||||
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
|
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
|
||||||
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
|
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
|
||||||
|
|||||||
@@ -16,7 +16,19 @@
|
|||||||
{% bootstrap_field form.internal_name layout="control" %}
|
{% bootstrap_field form.internal_name layout="control" %}
|
||||||
</div>
|
</div>
|
||||||
{% bootstrap_field form.description layout="control" %}
|
{% bootstrap_field form.description layout="control" %}
|
||||||
{% bootstrap_field form.is_addon layout="control" %}
|
{% bootstrap_field form.category_type layout="control" horizontal_field_class="big-radio-wrapper col-md-9" %}
|
||||||
|
<div class="row" data-display-dependency="#id_category_type_2">
|
||||||
|
<div class="col-md-offset-3 col-md-9">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Please note that cross-selling categories are intended as a marketing feature and are not
|
||||||
|
suitable for strictly ensuring that products are only available in certain combinations.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% bootstrap_field form.cross_selling_condition layout="control" horizontal_field_class="col-md-9" %}
|
||||||
|
{% bootstrap_field form.cross_selling_match_products layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
{% if category %}
|
{% if category %}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{{ i.default_price|money:request.event.currency }}
|
{{ i.default_price|money:request.event.currency }}
|
||||||
{% if i.original_price %}<strike class="text-muted">{{ i.original_price|money:request.event.currency }}</strike>{% endif %}
|
{% if i.original_price %}<strike class="text-muted">{{ i.original_price|money:request.event.currency }}</strike>{% endif %}
|
||||||
{% if i.tax_rule and i.default_price %}
|
{% if i.tax_rule %}
|
||||||
<br/>
|
<br/>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
{% if not i.tax_rule.price_includes_tax %}
|
{% if not i.tax_rule.price_includes_tax %}
|
||||||
|
|||||||
@@ -13,31 +13,52 @@
|
|||||||
{% trans "Edit question" %}
|
{% trans "Edit question" %}
|
||||||
</a>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
<form class="form-inline helper-display-inline" action="" method="get">
|
|
||||||
<p>
|
<div class="panel panel-default">
|
||||||
<select name="status" class="form-control">
|
<div class="panel-heading">
|
||||||
<option value="" {% if request.GET.status == "" %}selected="selected"{% endif %}>{% trans "All orders" %}</option>
|
<h3 class="panel-title">{% trans "Filter" %}</h3>
|
||||||
<option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
|
</div>
|
||||||
<option value="pv" {% if request.GET.status == "pv" %}selected="selected"{% endif %}>{% trans "Paid or confirmed" %}</option>
|
<form class="panel-body filter-form" action="" method="get">
|
||||||
<option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
|
<div class="row">
|
||||||
<option value="np" {% if request.GET.status == "np" or "status" not in request.GET %}selected="selected"{% endif %}>{% trans "Pending or paid" %}</option>
|
<div class="col-lg-2 col-sm-6 col-xs-6">
|
||||||
<option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>
|
<select name="status" class="form-control">
|
||||||
<option value="e" {% if request.GET.status == "e" %}selected="selected"{% endif %}>{% trans "Expired" %}</option>
|
<option value="" {% if request.GET.status == "" %}selected="selected"{% endif %}>{% trans "All orders" %}</option>
|
||||||
<option value="ne" {% if request.GET.status == "ne" %}selected="selected"{% endif %}>{% trans "Pending or expired" %}</option>
|
<option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
|
||||||
<option value="c" {% if request.GET.status == "c" %}selected="selected"{% endif %}>{% trans "Canceled" %}</option>
|
<option value="pv" {% if request.GET.status == "pv" %}selected="selected"{% endif %}>{% trans "Paid or confirmed" %}</option>
|
||||||
</select>
|
<option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
|
||||||
<select name="item" class="form-control">
|
<option value="np" {% if request.GET.status == "np" or "status" not in request.GET %}selected="selected"{% endif %}>{% trans "Pending or paid" %}</option>
|
||||||
<option value="">{% trans "All products" %}</option>
|
<option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>
|
||||||
{% for item in items %}
|
<option value="e" {% if request.GET.status == "e" %}selected="selected"{% endif %}>{% trans "Expired" %}</option>
|
||||||
<option value="{{ item.id }}"
|
<option value="ne" {% if request.GET.status == "ne" %}selected="selected"{% endif %}>{% trans "Pending or expired" %}</option>
|
||||||
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
|
<option value="c" {% if request.GET.status == "c" %}selected="selected"{% endif %}>{% trans "Canceled" %}</option>
|
||||||
{{ item.name }}
|
</select>
|
||||||
</option>
|
</div>
|
||||||
{% endfor %}
|
<div class="col-lg-5 col-sm-6 col-xs-6">
|
||||||
</select>
|
<select name="item" class="form-control">
|
||||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
<option value="">{% trans "All products" %}</option>
|
||||||
</p>
|
{% for item in items %}
|
||||||
</form>
|
<option value="{{ item.id }}"
|
||||||
|
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
|
||||||
|
{{ item.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% if request.event.has_subevents %}
|
||||||
|
<div class="col-lg-5 col-sm-6 col-xs-6">
|
||||||
|
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<button class="btn btn-primary btn-lg" type="submit">
|
||||||
|
<span class="fa fa-filter"></span>
|
||||||
|
{% trans "Filter" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row" id="question-stats">
|
<div class="row" id="question-stats">
|
||||||
{% if not stats %}
|
{% if not stats %}
|
||||||
<div class="empty-collection col-md-10 col-xs-12">
|
<div class="empty-collection col-md-10 col-xs-12">
|
||||||
@@ -75,7 +96,7 @@
|
|||||||
{% for stat in stats %}
|
{% for stat in stats %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status={{ request.GET.status|default:"np" }}&item={{ request.GET.item }}&question={{ question.pk }}&answer={{ stat.alink|default:stat.answer|urlencode }}">
|
<a href="{% url "control:event.orders" event=request.event.slug organizer=request.event.organizer.slug %}?status={{ request.GET.status|default:"np" }}&item={{ request.GET.item }}&subevent={{ request.GET.subevent }}&question={{ question.pk }}&answer={{ stat.alink|default:stat.answer|urlencode }}">
|
||||||
{{ stat.answer }}
|
{{ stat.answer }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -296,11 +296,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
<div class="formset" data-formset data-formset-prefix="{{ add_formset.prefix }}">
|
<div class="formset" data-formset data-formset-prefix="{{ add_position_formset.prefix }}">
|
||||||
{{ add_formset.management_form }}
|
{{ add_position_formset.management_form }}
|
||||||
{% bootstrap_formset_errors add_formset %}
|
{% bootstrap_formset_errors add_position_formset %}
|
||||||
<div data-formset-body>
|
<div data-formset-body>
|
||||||
{% for add_form in add_formset %}
|
{% for add_form in add_position_formset %}
|
||||||
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
@@ -351,25 +351,25 @@
|
|||||||
</button>
|
</button>
|
||||||
{% trans "Add product" %}
|
{% trans "Add product" %}
|
||||||
<div class="sr-only">
|
<div class="sr-only">
|
||||||
{{ add_formset.empty_form.id }}
|
{{ add_position_formset.empty_form.id }}
|
||||||
{% bootstrap_field add_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
{% bootstrap_field add_position_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="form-horizontal">
|
<div class="form-horizontal">
|
||||||
{% bootstrap_field add_formset.empty_form.itemvar layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.itemvar layout="control" %}
|
||||||
{% bootstrap_field add_formset.empty_form.price addon_after=request.event.currency layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.price addon_after=request.event.currency layout="control" %}
|
||||||
{% if add_formset.empty_form.addon_to %}
|
{% if add_position_formset.empty_form.addon_to %}
|
||||||
{% bootstrap_field add_formset.empty_form.addon_to layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.addon_to layout="control" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if add_formset.empty_form.subevent %}
|
{% if add_position_formset.empty_form.subevent %}
|
||||||
{% bootstrap_field add_formset.empty_form.subevent layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.subevent layout="control" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if add_formset.empty_form.used_membership %}
|
{% if add_position_formset.empty_form.used_membership %}
|
||||||
{% bootstrap_field add_formset.empty_form.used_membership layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.used_membership layout="control" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_field add_formset.empty_form.seat layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.seat layout="control" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -431,13 +431,77 @@
|
|||||||
{% bootstrap_field fee.form.operation_cancel layout='inline' %}
|
{% bootstrap_field fee.form.operation_cancel layout='inline' %}
|
||||||
{% if fee.fee_type == "payment" %}
|
{% if fee.fee_type == "payment" %}
|
||||||
<em class="text-danger">
|
<em class="text-danger">
|
||||||
{% trans "Manually modifying payment fees is discouraged since they might automatically be on subsequent order changes or when choosing a different payment method." %}
|
{% trans "Manually modifying payment fees is discouraged since they might automatically be updated on subsequent order changes or when choosing a different payment method." %}
|
||||||
</em>
|
</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="formset" data-formset data-formset-prefix="{{ add_fee_formset.prefix }}">
|
||||||
|
{{ add_fee_formset.management_form }}
|
||||||
|
{% bootstrap_formset_errors add_fee_formset %}
|
||||||
|
<div data-formset-body>
|
||||||
|
{% for add_form in add_fee_formset %}
|
||||||
|
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
<button type="button" class="btn btn-danger btn-xs pull-right flip"
|
||||||
|
data-formset-delete-button>
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
{% trans "Add fee" %}
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ add_form.id }}
|
||||||
|
{% bootstrap_field add_form.DELETE form_group_class="" layout="inline" %}
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="form-horizontal">
|
||||||
|
{% bootstrap_field add_form.fee_type layout='control' %}
|
||||||
|
{% bootstrap_field add_form.value addon_after=request.event.currency layout='control' %}
|
||||||
|
{% bootstrap_field add_form.tax_rule layout='control' %}
|
||||||
|
{% bootstrap_field add_form.description layout='control' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<script type="form-template" data-formset-empty-form>
|
||||||
|
{% escapescript %}
|
||||||
|
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
<button type="button" class="btn btn-danger btn-xs pull-right flip"
|
||||||
|
data-formset-delete-button>
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
{% trans "Add fee" %}
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ add_fee_formset.empty_form.id }}
|
||||||
|
{% bootstrap_field add_fee_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="form-horizontal">
|
||||||
|
{% bootstrap_field add_fee_formset.empty_form.fee_type layout='control' %}
|
||||||
|
{% bootstrap_field add_fee_formset.empty_form.value addon_after=request.event.currency layout='control' %}
|
||||||
|
{% bootstrap_field add_fee_formset.empty_form.tax_rule layout='control' %}
|
||||||
|
{% bootstrap_field add_fee_formset.empty_form.description layout='control' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endescapescript %}
|
||||||
|
</script>
|
||||||
|
<p>
|
||||||
|
<button type="button" class="btn btn-primary" data-formset-add>
|
||||||
|
<i class="fa fa-plus"></i> {% trans "Add fee" %}</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel panel-default items">
|
<div class="panel panel-default items">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
|
|||||||
@@ -198,7 +198,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
{% if order.status == "n" %}
|
{% if order.status == "n" and not order.require_approval %}
|
||||||
<dt>{% trans "Expiry date" %}</dt>
|
<dt>{% trans "Expiry date" %}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}
|
{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@
|
|||||||
{% trans "The waiting list is no longer active for this event. The waiting list no longer affects quotas and no longer notifies waiting users." %}
|
{% trans "The waiting list is no longer active for this event. The waiting list no longer affects quotas and no longer notifies waiting users." %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if request.event.settings.hide_sold_out %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{% trans "According to your event settings, sold out products are hidden from customers. This way, customers will not be able to discover the waiting list." %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% if 'can_change_orders' in request.eventpermset %}
|
{% if 'can_change_orders' in request.eventpermset %}
|
||||||
<form method="post" class="col-md-6"
|
<form method="post" class="col-md-6"
|
||||||
|
|||||||
@@ -94,7 +94,9 @@ def process_login(request, user, keep_logged_in):
|
|||||||
pretix_successful_logins.inc(1)
|
pretix_successful_logins.inc(1)
|
||||||
handle_login_source(user, request)
|
handle_login_source(user, request)
|
||||||
auth_login(request, user)
|
auth_login(request, user)
|
||||||
request.session['pretix_auth_login_time'] = int(time.time())
|
t = int(time.time())
|
||||||
|
request.session['pretix_auth_login_time'] = t
|
||||||
|
request.session['pretix_auth_last_used'] = t
|
||||||
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
|
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
|
||||||
return redirect_to_url(next_url)
|
return redirect_to_url(next_url)
|
||||||
return redirect('control:index')
|
return redirect('control:index')
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ from django.contrib import messages
|
|||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Exists, Max, OuterRef, Prefetch, Q, Subquery
|
from django.db.models import Exists, Max, OuterRef, Prefetch, Q, Subquery
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseNotAllowed, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -193,6 +193,9 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
|
|||||||
class CheckInListBulkRevertConfirmView(CheckInListQueryMixin, EventPermissionRequiredMixin, TemplateView):
|
class CheckInListBulkRevertConfirmView(CheckInListQueryMixin, EventPermissionRequiredMixin, TemplateView):
|
||||||
template_name = "pretixcontrol/checkin/bulk_revert_confirm.html"
|
template_name = "pretixcontrol/checkin/bulk_revert_confirm.html"
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
return HttpResponseNotAllowed(permitted_methods=["POST"])
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
|
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
@@ -296,7 +299,7 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related(
|
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related(
|
||||||
"limit_products", "auto_checkin_sales_channels"
|
"limit_products",
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.filter_form.is_valid():
|
if self.filter_form.is_valid():
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
|
|||||||
messages.success(self.request, _('Your changes have been saved.'))
|
messages.success(self.request, _('Your changes have been saved.'))
|
||||||
if form.has_changed():
|
if form.has_changed():
|
||||||
self.object.log_action(
|
self.object.log_action(
|
||||||
'pretix.event.category.reordered', user=self.request.user, data={
|
'pretix.event.category.changed', user=self.request.user, data={
|
||||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -302,6 +302,8 @@ class CategoryCreate(EventPermissionRequiredMixin, CreateView):
|
|||||||
i = modelcopy(self.copy_from)
|
i = modelcopy(self.copy_from)
|
||||||
i.pk = None
|
i.pk = None
|
||||||
kwargs['instance'] = i
|
kwargs['instance'] = i
|
||||||
|
kwargs.setdefault('initial', {})
|
||||||
|
kwargs['initial']['cross_selling_match_products'] = [str(i.pk) for i in self.copy_from.cross_selling_match_products.all()]
|
||||||
else:
|
else:
|
||||||
kwargs['instance'] = ItemCategory(event=self.request.event)
|
kwargs['instance'] = ItemCategory(event=self.request.event)
|
||||||
return kwargs
|
return kwargs
|
||||||
@@ -661,6 +663,10 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV
|
|||||||
question=self.object, orderposition__isnull=False,
|
question=self.object, orderposition__isnull=False,
|
||||||
orderposition__order__event=self.request.event
|
orderposition__order__event=self.request.event
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.request.GET.get("subevent", "") != "":
|
||||||
|
qs = qs.filter(orderposition__subevent=self.request.GET["subevent"])
|
||||||
|
|
||||||
s = self.request.GET.get("status", "np")
|
s = self.request.GET.get("status", "np")
|
||||||
if s != "":
|
if s != "":
|
||||||
if s == 'o':
|
if s == 'o':
|
||||||
@@ -914,16 +920,19 @@ class QuotaCreate(EventPermissionRequiredMixin, CreateView):
|
|||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
|
|
||||||
|
kwargs.setdefault('initial', {})
|
||||||
if self.copy_from:
|
if self.copy_from:
|
||||||
i = modelcopy(self.copy_from)
|
i = modelcopy(self.copy_from)
|
||||||
i.pk = None
|
i.pk = None
|
||||||
kwargs['instance'] = i
|
kwargs['instance'] = i
|
||||||
kwargs.setdefault('initial', {})
|
|
||||||
kwargs['initial']['itemvars'] = [str(i.pk) for i in self.copy_from.items.all()] + [
|
kwargs['initial']['itemvars'] = [str(i.pk) for i in self.copy_from.items.all()] + [
|
||||||
'{}-{}'.format(v.item_id, v.pk) for v in self.copy_from.variations.all()
|
'{}-{}'.format(v.item_id, v.pk) for v in self.copy_from.variations.all()
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
kwargs['instance'] = Quota(event=self.request.event)
|
kwargs['instance'] = Quota(event=self.request.event)
|
||||||
|
if 'product' in self.request.GET:
|
||||||
|
kwargs['initial']['itemvars'] = self.request.GET.getlist('product')
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def form_invalid(self, form):
|
def form_invalid(self, form):
|
||||||
|
|||||||
@@ -122,10 +122,11 @@ from pretix.control.forms.filter import (
|
|||||||
)
|
)
|
||||||
from pretix.control.forms.orders import (
|
from pretix.control.forms.orders import (
|
||||||
CancelForm, CommentForm, DenyForm, EventCancelForm, ExporterForm,
|
CancelForm, CommentForm, DenyForm, EventCancelForm, ExporterForm,
|
||||||
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
|
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeAddForm,
|
||||||
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
OrderFeeAddFormset, OrderFeeChangeForm, OrderLocaleForm, OrderMailForm,
|
||||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
OrderPositionAddForm, OrderPositionAddFormset, OrderPositionChangeForm,
|
||||||
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
|
OrderPositionMailForm, OrderRefundForm, OtherOperationsForm,
|
||||||
|
ReactivateOrderForm,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.rrule import RRuleForm
|
from pretix.control.forms.rrule import RRuleForm
|
||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
@@ -1874,18 +1875,30 @@ class OrderChange(OrderView):
|
|||||||
data=self.request.POST if self.request.method == "POST" else None)
|
data=self.request.POST if self.request.method == "POST" else None)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def add_formset(self):
|
def add_position_formset(self):
|
||||||
ff = formset_factory(
|
ff = formset_factory(
|
||||||
OrderPositionAddForm, formset=OrderPositionAddFormset,
|
OrderPositionAddForm, formset=OrderPositionAddFormset,
|
||||||
can_order=False, can_delete=True, extra=0
|
can_order=False, can_delete=True, extra=0
|
||||||
)
|
)
|
||||||
return ff(
|
return ff(
|
||||||
prefix='add',
|
prefix='add_position',
|
||||||
order=self.order,
|
order=self.order,
|
||||||
items=self.items,
|
items=self.items,
|
||||||
data=self.request.POST if self.request.method == "POST" else None
|
data=self.request.POST if self.request.method == "POST" else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def add_fee_formset(self):
|
||||||
|
ff = formset_factory(
|
||||||
|
OrderFeeAddForm, formset=OrderFeeAddFormset,
|
||||||
|
can_order=False, can_delete=True, extra=0
|
||||||
|
)
|
||||||
|
return ff(
|
||||||
|
prefix='add_fee',
|
||||||
|
order=self.order,
|
||||||
|
data=self.request.POST if self.request.method == "POST" else None
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def items(self):
|
def items(self):
|
||||||
return self.request.event.items.prefetch_related('variations', 'tax_rule').all()
|
return self.request.event.items.prefetch_related('variations', 'tax_rule').all()
|
||||||
@@ -1914,7 +1927,8 @@ class OrderChange(OrderView):
|
|||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['positions'] = self.positions
|
ctx['positions'] = self.positions
|
||||||
ctx['fees'] = self.fees
|
ctx['fees'] = self.fees
|
||||||
ctx['add_formset'] = self.add_formset
|
ctx['add_position_formset'] = self.add_position_formset
|
||||||
|
ctx['add_fee_formset'] = self.add_fee_formset
|
||||||
ctx['other_form'] = self.other_form
|
ctx['other_form'] = self.other_form
|
||||||
ctx['use_revocation_list'] = self.request.event.ticket_secret_generator.use_revocation_list
|
ctx['use_revocation_list'] = self.request.event.ticket_secret_generator.use_revocation_list
|
||||||
return ctx
|
return ctx
|
||||||
@@ -1929,12 +1943,35 @@ class OrderChange(OrderView):
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _process_add(self, ocm):
|
def _process_add_fees(self, ocm):
|
||||||
if not self.add_formset.is_valid():
|
if not self.add_fee_formset.is_valid():
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
for f in self.add_formset.forms:
|
for f in self.add_fee_formset.forms:
|
||||||
if f in self.add_formset.deleted_forms or not f.has_changed():
|
if f in self.add_fee_formset.deleted_forms or not f.has_changed():
|
||||||
|
continue
|
||||||
|
|
||||||
|
f = OrderFee(
|
||||||
|
fee_type=f.cleaned_data['fee_type'],
|
||||||
|
value=f.cleaned_data['value'],
|
||||||
|
order=ocm.order,
|
||||||
|
tax_rule=f.cleaned_data['tax_rule'],
|
||||||
|
description=f.cleaned_data['description'],
|
||||||
|
)
|
||||||
|
f._calculate_tax()
|
||||||
|
try:
|
||||||
|
ocm.add_fee(f)
|
||||||
|
except OrderError as e:
|
||||||
|
f.custom_error = str(e)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _process_add_positions(self, ocm):
|
||||||
|
if not self.add_position_formset.is_valid():
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
for f in self.add_position_formset.forms:
|
||||||
|
if f in self.add_position_formset.deleted_forms or not f.has_changed():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if '-' in f.cleaned_data['itemvar']:
|
if '-' in f.cleaned_data['itemvar']:
|
||||||
@@ -1959,7 +1996,7 @@ class OrderChange(OrderView):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _process_fees(self, ocm):
|
def _process_change_fees(self, ocm):
|
||||||
for f in self.fees:
|
for f in self.fees:
|
||||||
if not f.form.is_valid():
|
if not f.form.is_valid():
|
||||||
return False
|
return False
|
||||||
@@ -1980,7 +2017,7 @@ class OrderChange(OrderView):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _process_change(self, ocm):
|
def _process_change_positions(self, ocm):
|
||||||
for p in self.positions:
|
for p in self.positions:
|
||||||
if not p.form.is_valid():
|
if not p.form.is_valid():
|
||||||
return False
|
return False
|
||||||
@@ -2061,7 +2098,11 @@ class OrderChange(OrderView):
|
|||||||
notify=notify,
|
notify=notify,
|
||||||
reissue_invoice=self.other_form.cleaned_data['reissue_invoice'] if self.other_form.is_valid() else True
|
reissue_invoice=self.other_form.cleaned_data['reissue_invoice'] if self.other_form.is_valid() else True
|
||||||
)
|
)
|
||||||
form_valid = self._process_add(ocm) and self._process_fees(ocm) and self._process_change(ocm) and self._process_other(ocm)
|
form_valid = (self._process_add_fees(ocm) and
|
||||||
|
self._process_add_positions(ocm) and
|
||||||
|
self._process_change_fees(ocm) and
|
||||||
|
self._process_change_positions(ocm) and
|
||||||
|
self._process_other(ocm))
|
||||||
|
|
||||||
if not form_valid:
|
if not form_valid:
|
||||||
messages.error(self.request, _('An error occurred. Please see the details below.'))
|
messages.error(self.request, _('An error occurred. Please see the details below.'))
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ def event_list(request):
|
|||||||
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from'))
|
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from'))
|
||||||
).annotate(
|
).annotate(
|
||||||
order_from=Coalesce('min_from', 'date_from'),
|
order_from=Coalesce('min_from', 'date_from'),
|
||||||
).order_by('-order_from')
|
).order_by('-order_from', 'slug')
|
||||||
|
|
||||||
total = qs.count()
|
total = qs.count()
|
||||||
pagesize = 20
|
pagesize = 20
|
||||||
@@ -318,7 +318,7 @@ def nav_context_list(request):
|
|||||||
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from'))
|
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from'))
|
||||||
).annotate(
|
).annotate(
|
||||||
order_from=Coalesce('min_from', 'date_from'),
|
order_from=Coalesce('min_from', 'date_from'),
|
||||||
).order_by('-order_from')
|
).order_by('-order_from', 'slug')
|
||||||
|
|
||||||
if request.user.has_active_staff_session(request.session.session_key):
|
if request.user.has_active_staff_session(request.session.session_key):
|
||||||
qs_orga = Organizer.objects.all()
|
qs_orga = Organizer.objects.all()
|
||||||
|
|||||||
+2107
-1909
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -622,40 +622,40 @@ msgid ""
|
|||||||
"darker shade."
|
"darker shade."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||||
msgid "Search query"
|
msgid "Search query"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
+2168
-1903
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -656,40 +656,40 @@ msgid ""
|
|||||||
"darker shade."
|
"darker shade."
|
||||||
msgstr "تباين اللون سيئ للخلفية البيضاء، الرجاء اختيار لون غامق."
|
msgstr "تباين اللون سيئ للخلفية البيضاء، الرجاء اختيار لون غامق."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||||
msgid "Search query"
|
msgid "Search query"
|
||||||
msgstr "البحث في الاستفسارات"
|
msgstr "البحث في الاستفسارات"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "الكل"
|
msgstr "الكل"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr "لا شيء"
|
msgstr "لا شيء"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr "المختارة فقط"
|
msgstr "المختارة فقط"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "قم باستخدم اسم مختلف داخليا"
|
msgstr "قم باستخدم اسم مختلف داخليا"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "اضغط لاغلاق الصفحة"
|
msgstr "اضغط لاغلاق الصفحة"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "لم تقم بحفظ التعديلات!"
|
msgstr "لم تقم بحفظ التعديلات!"
|
||||||
|
|
||||||
|
|||||||
+2168
-1916
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
@@ -623,40 +623,40 @@ msgid ""
|
|||||||
"darker shade."
|
"darker shade."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||||
msgid "Search query"
|
msgid "Search query"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
+2160
-1900
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||||
"PO-Revision-Date: 2023-09-15 06:00+0000\n"
|
"PO-Revision-Date: 2023-09-15 06:00+0000\n"
|
||||||
"Last-Translator: Michael <michael.happl@gmx.at>\n"
|
"Last-Translator: Michael <michael.happl@gmx.at>\n"
|
||||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -650,40 +650,40 @@ msgstr ""
|
|||||||
"Tato barva je pro text na bílém pozadí špatně kontrastní, zvolte prosím "
|
"Tato barva je pro text na bílém pozadí špatně kontrastní, zvolte prosím "
|
||||||
"tmavší odstín."
|
"tmavší odstín."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||||
msgid "Search query"
|
msgid "Search query"
|
||||||
msgstr "Hledaný výraz"
|
msgstr "Hledaný výraz"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "Všechny"
|
msgstr "Všechny"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr "Žádný"
|
msgstr "Žádný"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr "Pouze vybrané"
|
msgstr "Pouze vybrané"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "Interně používat jiný název"
|
msgstr "Interně používat jiný název"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Kliknutím zavřete"
|
msgstr "Kliknutím zavřete"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "Máte neuložené změny!"
|
msgstr "Máte neuložené změny!"
|
||||||
|
|
||||||
|
|||||||
+2118
-1912
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -623,40 +623,40 @@ msgid ""
|
|||||||
"darker shade."
|
"darker shade."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||||
msgid "Search query"
|
msgid "Search query"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
+2150
-1906
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||||
"PO-Revision-Date: 2024-07-10 15:00+0000\n"
|
"PO-Revision-Date: 2024-07-10 15:00+0000\n"
|
||||||
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
|
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
|
||||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -670,40 +670,40 @@ msgid ""
|
|||||||
"darker shade."
|
"darker shade."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||||
msgid "Search query"
|
msgid "Search query"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||||
msgid "All"
|
msgid "All"
|
||||||
msgstr "Alle"
|
msgstr "Alle"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr "Ingen"
|
msgstr "Ingen"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Klik for at lukke"
|
msgstr "Klik for at lukke"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "Du har ændringer, der ikke er gemt!"
|
msgstr "Du har ændringer, der ikke er gemt!"
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user