mirror of
https://github.com/pretix/pretix.git
synced 2025-12-11 01:22:28 +00:00
Compare commits
1 Commits
fix-cond-d
...
bundles-no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9692283df |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system dependencies
|
||||
run: sudo apt update && sudo apt install -y gettext unzip
|
||||
run: sudo apt update && sudo apt install gettext unzip
|
||||
- name: Install Python dependencies
|
||||
run: pip3 install -U setuptools build pip check-manifest
|
||||
- name: Run check-manifest
|
||||
|
||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install -y enchant-2 hunspell aspell-en
|
||||
run: sudo apt update && sudo apt install enchant-2 hunspell aspell-en
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -Ur requirements.txt
|
||||
working-directory: ./doc
|
||||
|
||||
6
.github/workflows/strings.yml
vendored
6
.github/workflows/strings.yml
vendored
@@ -35,9 +35,9 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt -y install gettext
|
||||
run: sudo apt update && sudo apt install gettext
|
||||
- name: Install Dependencies
|
||||
run: pip3 install uv && uv pip install --system -e ".[dev]"
|
||||
run: pip3 install -e ".[dev]"
|
||||
- name: Compile messages
|
||||
run: python manage.py compilemessages
|
||||
working-directory: ./src
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install enchant-2 hunspell hunspell-de-de aspell-en aspell-de
|
||||
- name: Install Dependencies
|
||||
run: pip3 install uv && uv pip install --system -e ".[dev]"
|
||||
run: pip3 install -e ".[dev]"
|
||||
- name: Spellcheck translations
|
||||
run: potypo
|
||||
working-directory: ./src
|
||||
|
||||
4
.github/workflows/style.yml
vendored
4
.github/workflows/style.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||
run: pip3 install -e ".[dev]" psycopg2-binary
|
||||
- name: Run isort
|
||||
run: isort -c .
|
||||
working-directory: ./src
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||
run: pip3 install -e ".[dev]" psycopg2-binary
|
||||
- name: Run flake8
|
||||
run: flake8 .
|
||||
working-directory: ./src
|
||||
|
||||
32
.github/workflows/tests.yml
vendored
32
.github/workflows/tests.yml
vendored
@@ -30,21 +30,15 @@ jobs:
|
||||
python-version: "3.9"
|
||||
- database: sqlite
|
||||
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:
|
||||
- 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 }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -56,9 +50,9 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system dependencies
|
||||
run: sudo apt update && sudo apt install -y gettext
|
||||
run: sudo apt update && sudo apt install gettext
|
||||
- name: Install Python dependencies
|
||||
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||
run: pip3 install --ignore-requires-python -e ".[dev]" psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
|
||||
- name: Run checks
|
||||
run: python manage.py check
|
||||
working-directory: ./src
|
||||
@@ -70,15 +64,15 @@ jobs:
|
||||
run: make all compress
|
||||
- name: Run tests
|
||||
working-directory: ./src
|
||||
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml tests --maxfail=100
|
||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
|
||||
- name: Run concurrency tests
|
||||
working-directory: ./src
|
||||
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reuse-db
|
||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
|
||||
if: matrix.database == 'postgres'
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: src/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
fail_ci_if_error: true
|
||||
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
||||
|
||||
@@ -10,7 +10,7 @@ tests:
|
||||
- cd src
|
||||
- python manage.py check
|
||||
- make all compress
|
||||
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
|
||||
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
|
||||
except:
|
||||
- pypi
|
||||
pypi:
|
||||
|
||||
@@ -71,7 +71,7 @@ Endpoints
|
||||
"mode": "placed",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"all_products": false,
|
||||
"all_products": False,
|
||||
"limit_products": [2, 3],
|
||||
"limit_variations": [456],
|
||||
"all_payment_methods": true,
|
||||
@@ -113,7 +113,7 @@ Endpoints
|
||||
"mode": "placed",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"all_products": false,
|
||||
"all_products": False,
|
||||
"limit_products": [2, 3],
|
||||
"limit_variations": [456],
|
||||
"all_payment_methods": true,
|
||||
@@ -146,7 +146,7 @@ Endpoints
|
||||
"mode": "placed",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"all_products": false,
|
||||
"all_products": False,
|
||||
"limit_products": [2, 3],
|
||||
"limit_variations": [456],
|
||||
"all_payment_methods": true,
|
||||
@@ -167,7 +167,7 @@ Endpoints
|
||||
"mode": "placed",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"all_products": false,
|
||||
"all_products": False,
|
||||
"limit_products": [2, 3],
|
||||
"limit_variations": [456],
|
||||
"all_payment_methods": true,
|
||||
@@ -216,7 +216,7 @@ Endpoints
|
||||
"mode": "placed",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"all_products": false,
|
||||
"all_products": False,
|
||||
"limit_products": [2, 3],
|
||||
"limit_variations": [456],
|
||||
"all_payment_methods": true,
|
||||
|
||||
@@ -23,22 +23,6 @@ position integer An integer, use
|
||||
is_addon boolean If ``true``, items within this category are not on sale
|
||||
on their own but the category provides a source for
|
||||
defining add-ons for other products.
|
||||
cross_selling_mode string If ``null``, cross-selling is disabled for this category.
|
||||
If ``"only"``, it is only visible in the cross-selling
|
||||
step.
|
||||
If ``"both"``, it is visible on the normal index page
|
||||
as well.
|
||||
Only available if ``is_addon`` is ``false``.
|
||||
cross_selling_condition string Only relevant if ``cross_selling_mode`` is not ``null``.
|
||||
If ``"always"``, always show in cross-selling step.
|
||||
If ``"products"``, only show if the cart contains one of
|
||||
the products listed in ``cross_selling_match_products``.
|
||||
If ``"discounts"``, only show products that qualify for
|
||||
a discount according to discount rules.
|
||||
cross_selling_match_products list of integer Only relevant if ``cross_selling_condition`` is
|
||||
``"products"``. Internal ID of the items of which at
|
||||
least one needs to be in the cart for this category to
|
||||
be shown.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -76,10 +60,7 @@ Endpoints
|
||||
"internal_name": "",
|
||||
"description": {"en": "Tickets are what you need to get in."},
|
||||
"position": 1,
|
||||
"is_addon": false,
|
||||
"cross_selling_mode": null,
|
||||
"cross_selling_condition": null,
|
||||
"cross_selling_match_products": []
|
||||
"is_addon": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -121,10 +102,7 @@ Endpoints
|
||||
"internal_name": "",
|
||||
"description": {"en": "Tickets are what you need to get in."},
|
||||
"position": 1,
|
||||
"is_addon": false,
|
||||
"cross_selling_mode": null,
|
||||
"cross_selling_condition": null,
|
||||
"cross_selling_match_products": []
|
||||
"is_addon": false
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -152,10 +130,7 @@ Endpoints
|
||||
"internal_name": "",
|
||||
"description": {"en": "Tickets are what you need to get in."},
|
||||
"position": 1,
|
||||
"is_addon": false,
|
||||
"cross_selling_mode": null,
|
||||
"cross_selling_condition": null,
|
||||
"cross_selling_match_products": []
|
||||
"is_addon": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -172,10 +147,7 @@ Endpoints
|
||||
"internal_name": "",
|
||||
"description": {"en": "Tickets are what you need to get in."},
|
||||
"position": 1,
|
||||
"is_addon": false,
|
||||
"cross_selling_mode": null,
|
||||
"cross_selling_condition": null,
|
||||
"cross_selling_match_products": []
|
||||
"is_addon": false
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event to create a category for
|
||||
@@ -221,10 +193,7 @@ Endpoints
|
||||
"internal_name": "",
|
||||
"description": {"en": "Tickets are what you need to get in."},
|
||||
"position": 1,
|
||||
"is_addon": true,
|
||||
"cross_selling_mode": null,
|
||||
"cross_selling_condition": null,
|
||||
"cross_selling_match_products": []
|
||||
"is_addon": true
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
|
||||
@@ -206,17 +206,6 @@ checkins list of objects List of **succe
|
||||
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
|
||||
├ device_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
|
||||
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
|
||||
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
|
||||
└ url string Download URL
|
||||
@@ -244,10 +233,6 @@ pdf_data object Data object req
|
||||
|
||||
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
|
||||
@@ -414,21 +399,10 @@ List of all orders
|
||||
"type": "entry",
|
||||
"gate": null,
|
||||
"device": 2,
|
||||
"device_id": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"auto_checked_in": false
|
||||
}
|
||||
],
|
||||
"print_logs": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "badge",
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"device_id": 1,
|
||||
"source": "pretixSCAN",
|
||||
"info": {}
|
||||
}
|
||||
],
|
||||
"answers": [
|
||||
{
|
||||
"question": 12,
|
||||
@@ -652,22 +626,10 @@ Fetching individual orders
|
||||
"type": "entry",
|
||||
"gate": null,
|
||||
"device": 2,
|
||||
"device_id": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"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": [
|
||||
{
|
||||
"question": 12,
|
||||
@@ -1015,8 +977,8 @@ Creating orders
|
||||
* ``internal_reference``
|
||||
* ``vat_id``
|
||||
* ``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
|
||||
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
||||
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!
|
||||
|
||||
* ``positions``
|
||||
|
||||
@@ -1619,22 +1581,10 @@ List of all order positions
|
||||
"type": "entry",
|
||||
"gate": null,
|
||||
"device": 2,
|
||||
"device_id": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"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": [
|
||||
{
|
||||
"question": 12,
|
||||
@@ -1745,22 +1695,10 @@ Fetching individual positions
|
||||
"type": "entry",
|
||||
"gate": null,
|
||||
"device": 2,
|
||||
"device_id": 1,
|
||||
"datetime": "2017-12-25T12:45:23Z",
|
||||
"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": [
|
||||
{
|
||||
"question": 12,
|
||||
@@ -1857,10 +1795,6 @@ Manipulating individual positions
|
||||
|
||||
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)/
|
||||
|
||||
Updates specific fields on an order position. Currently, only the following fields are supported:
|
||||
@@ -2120,59 +2054,6 @@ Manipulating individual positions
|
||||
:statuscode 401: Authentication failure
|
||||
: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
|
||||
-----------------------
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ Endpoints
|
||||
"results": [
|
||||
{
|
||||
"identifier": "web",
|
||||
"label": {
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
@@ -88,7 +88,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "web",
|
||||
"label": {
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
@@ -116,7 +116,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "api.custom",
|
||||
"label": {
|
||||
"name": {
|
||||
"en": "Custom integration"
|
||||
},
|
||||
"type": "api",
|
||||
@@ -133,7 +133,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "api.custom",
|
||||
"label": {
|
||||
"name": {
|
||||
"en": "Custom integration"
|
||||
},
|
||||
"type": "api",
|
||||
@@ -178,7 +178,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"identifier": "web",
|
||||
"label": {
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"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.
|
||||
|
||||
Endpoints for organizer 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.
|
||||
|
||||
|
||||
.. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3
|
||||
.. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3
|
||||
@@ -1,4 +1,4 @@
|
||||
.. _`rest-seats`:
|
||||
.. _`rest-reusablemedia`:
|
||||
|
||||
Seats
|
||||
=====
|
||||
|
||||
@@ -136,7 +136,6 @@ Endpoints
|
||||
}
|
||||
|
||||
: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 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.
|
||||
@@ -468,7 +467,6 @@ Endpoints
|
||||
}
|
||||
|
||||
: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 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.
|
||||
|
||||
@@ -17,7 +17,6 @@ First, you need to declare that you are using non-essential cookies by respondin
|
||||
signal:
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:no-index:
|
||||
:members: register_cookie_providers
|
||||
|
||||
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,
|
||||
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
||||
register_ticket_secret_generators, gift_card_transaction_display,
|
||||
register_text_placeholders, register_mail_placeholders, device_info_updated
|
||||
register_text_placeholders, register_mail_placeholders
|
||||
|
||||
Order events
|
||||
""""""""""""
|
||||
@@ -22,14 +22,12 @@ Order events
|
||||
There are multiple signals that will be sent out in the ordering cycle:
|
||||
|
||||
.. 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
|
||||
|
||||
Check-ins
|
||||
"""""""""
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
:members: checkin_created
|
||||
|
||||
|
||||
@@ -41,21 +39,18 @@ Frontend
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:no-index:
|
||||
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
|
||||
|
||||
Request flow
|
||||
""""""""""""
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:no-index:
|
||||
:members: process_request, process_response
|
||||
|
||||
Vouchers
|
||||
""""""""
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:no-index:
|
||||
:members: voucher_redeem_info
|
||||
|
||||
Backend
|
||||
@@ -67,28 +62,24 @@ Backend
|
||||
item_formsets, order_search_filter_q, order_search_forms
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
|
||||
|
||||
Vouchers
|
||||
""""""""
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:no-index:
|
||||
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
|
||||
|
||||
Dashboards
|
||||
""""""""""
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:no-index:
|
||||
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
|
||||
|
||||
Ticket designs
|
||||
""""""""""""""
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
:members: layout_text_variables, layout_image_variables
|
||||
|
||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
||||
@@ -98,5 +89,4 @@ API
|
||||
---
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
:members: validate_event_settings, api_event_settings_fields
|
||||
|
||||
@@ -60,7 +60,6 @@ that we'll provide in this plugin:
|
||||
Similar signals exist for other objects:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
:members: voucher_import_columns
|
||||
|
||||
|
||||
|
||||
@@ -84,6 +84,8 @@ convenient to you:
|
||||
|
||||
.. automethod:: _register_fonts
|
||||
|
||||
.. automethod:: _register_event_fonts
|
||||
|
||||
.. automethod:: _on_first_page
|
||||
|
||||
.. automethod:: _on_other_page
|
||||
|
||||
@@ -86,10 +86,7 @@ Signals
|
||||
-------
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
:members: register_text_placeholders
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
:members: register_mail_placeholders
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
KulturPass
|
||||
==========
|
||||
=========
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ expects and - more importantly - supports.
|
||||
for a sample configuration in an academic context.
|
||||
|
||||
Note, that you can have multiple attributes with the same ``friendlyName``
|
||||
but different ``name`` value. This is often used in systems, where the same
|
||||
but different ``name``s. This is often used in systems, where the same
|
||||
information (for example a persons name) is saved in different fields -
|
||||
for example because one institution is returning SAML 1.0 and other
|
||||
institutions are returning SAML 2.0 style attributes. Typically, this only
|
||||
|
||||
@@ -29,8 +29,8 @@ item_assignments list of objects Products this l
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
Layout endpoints
|
||||
----------------
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/ticketlayouts/
|
||||
|
||||
@@ -268,75 +268,5 @@ Layout endpoints
|
||||
:statuscode 401: Authentication failure
|
||||
: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
|
||||
|
||||
@@ -43,7 +43,7 @@ dependencies = [
|
||||
"django-formset-js-improved==0.5.0.3",
|
||||
"django-formtools==2.5.1",
|
||||
"django-hierarkey==1.2.*",
|
||||
"django-hijack==3.7.*",
|
||||
"django-hijack==3.6.*",
|
||||
"django-i18nfield==1.9.*,>=1.9.4",
|
||||
"django-libsass==0.9",
|
||||
"django-localflavor==4.0",
|
||||
@@ -55,7 +55,7 @@ dependencies = [
|
||||
"django-scopes==2.0.*",
|
||||
"django-statici18n==2.5.*",
|
||||
"djangorestframework==3.15.*",
|
||||
"dnspython==2.7.*",
|
||||
"dnspython==2.6.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
"geoip2==4.*",
|
||||
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
@@ -74,24 +74,24 @@ dependencies = [
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.9.*",
|
||||
"phonenumberslite==8.13.*",
|
||||
"Pillow==11.0.*",
|
||||
"Pillow==10.4.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==5.28.*",
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
"pycparser==2.22",
|
||||
"pycryptodome==3.21.*",
|
||||
"pypdf==5.1.*",
|
||||
"pypdf==5.0.*",
|
||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.9.*",
|
||||
"pytz",
|
||||
"pytz-deprecation-shim==0.1.*",
|
||||
"pyuca",
|
||||
"qrcode==8.0",
|
||||
"redis==5.2.*",
|
||||
"redis==5.1.*",
|
||||
"reportlab==4.2.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==2.17.*",
|
||||
"sentry-sdk==2.15.*",
|
||||
"sepaxml==2.6.*",
|
||||
"slimit",
|
||||
"stripe==7.9.*",
|
||||
@@ -102,7 +102,7 @@ dependencies = [
|
||||
"vat_moss_forked==2020.3.20.0.11.0",
|
||||
"vobject==0.9.*",
|
||||
"webauthn==2.2.*",
|
||||
"zeep==4.3.*"
|
||||
"zeep==4.2.*"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -111,17 +111,18 @@ dev = [
|
||||
"aiohttp==3.10.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.26.*",
|
||||
"fakeredis==2.24.*",
|
||||
"flake8==7.1.*",
|
||||
"freezegun",
|
||||
"isort==5.13.*",
|
||||
"pep8-naming==0.14.*",
|
||||
"potypo",
|
||||
"pytest-asyncio>=0.24",
|
||||
"pytest-asyncio",
|
||||
"pytest-cache",
|
||||
"pytest-cov",
|
||||
"pytest-django==4.*",
|
||||
"pytest-mock==3.14.*",
|
||||
"pytest-rerunfailures==14.*",
|
||||
"pytest-sugar",
|
||||
"pytest-xdist==3.6.*",
|
||||
"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
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "2024.11.0.dev0"
|
||||
__version__ = "2024.10.0.dev0"
|
||||
|
||||
@@ -77,7 +77,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:blockedsecrets-list'),
|
||||
('GET', 'api-v1:order-list'),
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
('POST', 'api-v1:orderposition-printlog'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
@@ -113,7 +112,6 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:blockedsecrets-list'),
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
('POST', 'api-v1:orderposition-printlog'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
@@ -149,7 +147,6 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:revokedsecrets-list'),
|
||||
('GET', 'api-v1:blockedsecrets-list'),
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
('POST', 'api-v1:orderposition-printlog'),
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
@@ -191,7 +188,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:orderposition-list'),
|
||||
('GET', 'api-v1:orderposition-answer'),
|
||||
('GET', 'api-v1:orderposition-pdf_image'),
|
||||
('POST', 'api-v1:orderposition-printlog'),
|
||||
('POST', 'api-v1:order-mark-canceled'),
|
||||
('POST', 'api-v1:orderpayment-list'),
|
||||
('POST', 'api-v1:orderrefund-list'),
|
||||
|
||||
@@ -88,20 +88,16 @@ class SalesChannelMigrationMixin:
|
||||
}
|
||||
|
||||
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
||||
raise ValidationError({
|
||||
"limit_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."
|
||||
]
|
||||
})
|
||||
raise ValidationError(
|
||||
"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"]):
|
||||
raise ValidationError({
|
||||
"limit_sales_channels": [
|
||||
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the same list."
|
||||
]
|
||||
})
|
||||
raise ValidationError(
|
||||
"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:
|
||||
data["all_sales_channels"] = True
|
||||
@@ -110,10 +106,6 @@ class SalesChannelMigrationMixin:
|
||||
data["all_sales_channels"] = False
|
||||
data["limit_sales_channels"] = data["sales_channels"]
|
||||
del data["sales_channels"]
|
||||
|
||||
if data.get("all_sales_channels"):
|
||||
data["limit_sales_channels"] = []
|
||||
|
||||
return super().to_internal_value(data)
|
||||
|
||||
def to_representation(self, value):
|
||||
|
||||
@@ -772,7 +772,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'invoice_address_company_required',
|
||||
'invoice_address_beneficiary',
|
||||
'invoice_address_custom_field',
|
||||
'invoice_address_custom_field_helptext',
|
||||
'invoice_name_required',
|
||||
'invoice_address_not_asked_free',
|
||||
'invoice_show_payments',
|
||||
@@ -917,7 +916,6 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
'invoice_address_company_required',
|
||||
'invoice_address_beneficiary',
|
||||
'invoice_address_custom_field',
|
||||
'invoice_address_custom_field_helptext',
|
||||
'invoice_name_required',
|
||||
'invoice_address_not_asked_free',
|
||||
'invoice_address_from_name',
|
||||
|
||||
@@ -369,7 +369,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||
item = Item.objects.create(**validated_data)
|
||||
if limit_sales_channels and not validated_data.get('all_sales_channels'):
|
||||
if limit_sales_channels:
|
||||
item.limit_sales_channels.add(*limit_sales_channels)
|
||||
if picture:
|
||||
item.picture.save(os.path.basename(picture.name), picture)
|
||||
@@ -441,22 +441,7 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ItemCategory
|
||||
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
|
||||
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon')
|
||||
|
||||
|
||||
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -55,7 +55,7 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
PrintLog, RevokedTicketSecret,
|
||||
RevokedTicketSecret,
|
||||
)
|
||||
from pretix.base.pdf import get_images, get_variables
|
||||
from pretix.base.services.cart import error_messages
|
||||
@@ -284,26 +284,6 @@ class CheckinSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
||||
|
||||
|
||||
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):
|
||||
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
|
||||
raw_barcode = serializers.CharField(required=True, allow_null=False)
|
||||
@@ -496,7 +476,6 @@ class OrderPositionListSerializer(serializers.ListSerializer):
|
||||
|
||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
checkins = CheckinSerializer(many=True, read_only=True)
|
||||
print_logs = PrintLogSerializer(many=True, read_only=True)
|
||||
answers = AnswerSerializer(many=True)
|
||||
downloads = PositionDownloadsField(source='*', read_only=True)
|
||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||
@@ -511,7 +490,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
||||
read_only_fields = (
|
||||
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
||||
@@ -598,9 +577,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat',
|
||||
'require_attention', 'order__status', 'order__valid_if_pending', 'order__require_approval',
|
||||
'valid_from', 'valid_until', 'blocked')
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
|
||||
'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
|
||||
'blocked')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -1515,7 +1494,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
pos.answers = answers
|
||||
pos.pseudonymization_id = "PREVIEW"
|
||||
pos.checkins = []
|
||||
pos.print_logs = []
|
||||
pos_map[pos.positionid] = pos
|
||||
else:
|
||||
if pos.voucher:
|
||||
|
||||
@@ -62,7 +62,6 @@ from pretix.base.models import (
|
||||
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
|
||||
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.orders import PrintLog
|
||||
from pretix.base.services.checkin import (
|
||||
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||
)
|
||||
@@ -366,9 +365,8 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch(
|
||||
lookup='checkins',
|
||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
|
||||
),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||
@@ -380,7 +378,6 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
||||
'positions',
|
||||
OrderPosition.objects.prefetch_related(
|
||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||
'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
)
|
||||
)
|
||||
@@ -392,9 +389,8 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch(
|
||||
lookup='checkins',
|
||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
|
||||
),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import base64
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from cryptography.hazmat.backends.openssl.backend import Backend
|
||||
@@ -147,8 +146,6 @@ class InitializeView(APIView):
|
||||
permission_classes = ()
|
||||
|
||||
def post(self, request, format=None):
|
||||
from pretix.base.signals import device_info_updated
|
||||
|
||||
serializer = InitializationRequestSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
@@ -163,8 +160,6 @@ class InitializeView(APIView):
|
||||
if device.revoked:
|
||||
raise ValidationError({'token': ['This initialization token has been revoked.']})
|
||||
|
||||
old_instance = copy.copy(device)
|
||||
|
||||
device.initialized = now()
|
||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||
@@ -179,10 +174,6 @@ class InitializeView(APIView):
|
||||
|
||||
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
|
||||
|
||||
device_info_updated.send(
|
||||
sender=Device, old_device=old_instance, new_device=device
|
||||
)
|
||||
|
||||
serializer = DeviceSerializer(device)
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -191,12 +182,9 @@ class UpdateView(APIView):
|
||||
authentication_classes = (DeviceTokenAuthentication,)
|
||||
|
||||
def post(self, request, format=None):
|
||||
from pretix.base.signals import device_info_updated
|
||||
|
||||
serializer = UpdateRequestSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
device = request.auth
|
||||
old_instance = copy.copy(device)
|
||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||
device.os_name = serializer.validated_data.get('os_name')
|
||||
@@ -212,10 +200,6 @@ class UpdateView(APIView):
|
||||
device.save()
|
||||
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)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@@ -297,8 +297,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
|
||||
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']
|
||||
if not new_event.all_sales_channels:
|
||||
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
||||
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
||||
else:
|
||||
serializer.instance.set_defaults()
|
||||
|
||||
@@ -371,7 +370,7 @@ with scopes_disabled():
|
||||
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
fields = ['is_public', 'active', 'event__live']
|
||||
fields = ['active', 'event__live']
|
||||
|
||||
def ends_after_qs(self, queryset, name, value):
|
||||
expr = Q(
|
||||
|
||||
@@ -42,7 +42,6 @@ from pretix.base.models import (
|
||||
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
|
||||
ReusableMedium,
|
||||
)
|
||||
from pretix.base.models.orders import PrintLog
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
@@ -80,7 +79,6 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
||||
'order', 'order__event', 'order__event__organizer', 'seat',
|
||||
).prefetch_related(
|
||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
)
|
||||
),
|
||||
|
||||
@@ -57,8 +57,7 @@ from pretix.api.serializers.order import (
|
||||
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
||||
OrderPositionSerializer, OrderRefundCreateSerializer,
|
||||
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
||||
PrintLogSerializer, RevokedTicketSecretSerializer,
|
||||
SimulatedOrderSerializer,
|
||||
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
|
||||
)
|
||||
from pretix.api.serializers.orderchange import (
|
||||
BlockNameSerializer, OrderChangeOperationSerializer,
|
||||
@@ -76,7 +75,7 @@ from pretix.base.models import (
|
||||
TeamAPIToken, generate_secret,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret,
|
||||
BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret,
|
||||
)
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.pdf import get_images
|
||||
@@ -260,7 +259,6 @@ class OrderViewSetMixin:
|
||||
'positions',
|
||||
opq.all().prefetch_related(
|
||||
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('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
|
||||
)),
|
||||
@@ -282,7 +280,6 @@ class OrderViewSetMixin:
|
||||
'positions',
|
||||
opq.all().prefetch_related(
|
||||
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||
'item', 'variation',
|
||||
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
|
||||
'seat',
|
||||
@@ -1096,7 +1093,6 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
qs = qs.prefetch_related(
|
||||
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('meta_values', ItemMetaValue.objects.select_related('property'),
|
||||
to_attr='meta_values_cached')
|
||||
@@ -1140,7 +1136,6 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
||||
else:
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
).select_related(
|
||||
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
||||
@@ -1259,34 +1254,6 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
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>[^/]+)')
|
||||
def pdf_image(self, request, key, **kwargs):
|
||||
pos = self.get_object()
|
||||
|
||||
@@ -1122,7 +1122,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
|
||||
if 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:
|
||||
del self.fields['custom_field']
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ import time
|
||||
import traceback
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.dispatch.dispatcher import NO_RECEIVERS
|
||||
|
||||
@@ -58,8 +57,6 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
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:
|
||||
return
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# Generated by Django 4.2.11 on 2024-05-27 13:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.orders
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0270_historicpassword"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="itemcategory",
|
||||
name="cross_selling_condition",
|
||||
field=models.CharField(null=True, max_length=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="itemcategory",
|
||||
name="cross_selling_mode",
|
||||
field=models.CharField(null=True, max_length=5),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="itemcategory",
|
||||
name="cross_selling_match_products",
|
||||
field=models.ManyToManyField(
|
||||
related_name="matched_by_cross_selling_categories", to="pretixbase.item"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,79 +0,0 @@
|
||||
# Generated by Django 4.2.16 on 2024-09-19 10:41
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
("pretixbase", "0271_itemcategory_cross_selling"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="PrintLog",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("datetime", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("created", models.DateTimeField(auto_now_add=True, null=True)),
|
||||
("successful", models.BooleanField(default=True)),
|
||||
("source", models.CharField(max_length=255)),
|
||||
("type", models.CharField(max_length=255)),
|
||||
("info", models.JSONField(default=dict)),
|
||||
(
|
||||
"api_token",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="pretixbase.teamapitoken",
|
||||
),
|
||||
),
|
||||
(
|
||||
"device",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="print_logs",
|
||||
to="pretixbase.device",
|
||||
),
|
||||
),
|
||||
(
|
||||
"oauth_application",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"position",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="print_logs",
|
||||
to="pretixbase.orderposition",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="print_logs",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ("-datetime",),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -20,11 +20,11 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from collections import defaultdict, namedtuple
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from itertools import groupby
|
||||
from math import ceil, inf
|
||||
from typing import Dict
|
||||
from math import ceil
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator
|
||||
@@ -36,8 +36,6 @@ from django_scopes import ScopedManager
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models.base import LoggedModel
|
||||
|
||||
PositionInfo = namedtuple('PositionInfo', ['item_id', 'subevent_id', 'line_price_gross', 'is_addon_to', 'voucher_discount'])
|
||||
|
||||
|
||||
class Discount(LoggedModel):
|
||||
SUBEVENT_MODE_MIXED = 'mixed'
|
||||
@@ -247,26 +245,22 @@ class Discount(LoggedModel):
|
||||
return False
|
||||
return True
|
||||
|
||||
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].line_price_gross for idx in condition_idx_group) < self.condition_min_value:
|
||||
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result):
|
||||
if self.condition_min_value and sum(positions[idx][2] for idx in condition_idx_group) < self.condition_min_value:
|
||||
return
|
||||
|
||||
if self.condition_min_count or self.benefit_only_apply_to_cheapest_n_matches:
|
||||
raise ValueError('Validation invariant violated.')
|
||||
|
||||
for idx in benefit_idx_group:
|
||||
previous_price = positions[idx].line_price_gross
|
||||
previous_price = positions[idx][2]
|
||||
new_price = round_decimal(
|
||||
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
|
||||
self.event.currency,
|
||||
)
|
||||
result[idx] = new_price
|
||||
|
||||
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):
|
||||
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result):
|
||||
if len(condition_idx_group) < self.condition_min_count:
|
||||
return
|
||||
|
||||
@@ -274,53 +268,23 @@ class Discount(LoggedModel):
|
||||
raise ValueError('Validation invariant violated.')
|
||||
|
||||
if self.benefit_only_apply_to_cheapest_n_matches:
|
||||
# sort by line_price
|
||||
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))
|
||||
if not self.condition_min_count:
|
||||
raise ValueError('Validation invariant violated.')
|
||||
|
||||
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
|
||||
# want to match multiples of 3
|
||||
|
||||
# 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)
|
||||
n_groups = min(len(condition_idx_group) // self.condition_min_count, ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches))
|
||||
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]
|
||||
|
||||
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:
|
||||
consume_idx = condition_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:
|
||||
previous_price = positions[idx].line_price_gross
|
||||
previous_price = positions[idx][2]
|
||||
new_price = round_decimal(
|
||||
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
|
||||
self.event.currency,
|
||||
@@ -328,16 +292,15 @@ class Discount(LoggedModel):
|
||||
result[idx] = new_price
|
||||
|
||||
for idx in consume_idx:
|
||||
result.setdefault(idx, positions[idx].line_price_gross)
|
||||
result.setdefault(idx, positions[idx][2])
|
||||
|
||||
def apply(self, positions: Dict[int, PositionInfo],
|
||||
collect_potential_discounts=None) -> Dict[int, Decimal]:
|
||||
def apply(self, positions: Dict[int, Tuple[int, Optional[int], Decimal, bool, Decimal]]) -> Dict[int, Decimal]:
|
||||
"""
|
||||
Tries to apply this discount to a cart
|
||||
|
||||
:param positions: Dictionary mapping IDs to PositionInfo tuples.
|
||||
:param positions: Dictionary mapping IDs to tuples of the form
|
||||
``(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)``.
|
||||
Bundled positions may not be included.
|
||||
: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
|
||||
contained in this dictionary are considered "consumed" and should not be considered
|
||||
@@ -379,13 +342,13 @@ class Discount(LoggedModel):
|
||||
|
||||
if self.subevent_mode == self.SUBEVENT_MODE_MIXED: # also applies to non-series events
|
||||
if self.condition_min_count:
|
||||
self._apply_min_count(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
|
||||
self._apply_min_count(positions, condition_candidates, benefit_candidates, result)
|
||||
else:
|
||||
self._apply_min_value(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
|
||||
self._apply_min_value(positions, condition_candidates, benefit_candidates, result)
|
||||
|
||||
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
|
||||
def key(idx):
|
||||
return positions[idx].subevent_id or 0
|
||||
return positions[idx][1] or 0 # subevent_id
|
||||
|
||||
# Build groups of candidates with the same subevent, then apply our regular algorithm
|
||||
# to each group
|
||||
@@ -394,11 +357,11 @@ class Discount(LoggedModel):
|
||||
candidate_groups = [(k, list(g)) for k, g in _groups]
|
||||
|
||||
for subevent_id, g in candidate_groups:
|
||||
benefit_g = [idx for idx in benefit_candidates if positions[idx].subevent_id == subevent_id]
|
||||
benefit_g = [idx for idx in benefit_candidates if positions[idx][1] == subevent_id]
|
||||
if self.condition_min_count:
|
||||
self._apply_min_count(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
|
||||
self._apply_min_count(positions, g, benefit_g, result)
|
||||
else:
|
||||
self._apply_min_value(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
|
||||
self._apply_min_value(positions, g, benefit_g, result)
|
||||
|
||||
elif self.subevent_mode == self.SUBEVENT_MODE_DISTINCT:
|
||||
if self.condition_min_value or not self.benefit_same_products:
|
||||
@@ -414,9 +377,9 @@ class Discount(LoggedModel):
|
||||
# Build a list of subevent IDs in descending order of frequency
|
||||
subevent_to_idx = defaultdict(list)
|
||||
for idx, p in positions.items():
|
||||
subevent_to_idx[p.subevent_id].append(idx)
|
||||
subevent_to_idx[p[1]].append(idx)
|
||||
for v in subevent_to_idx.values():
|
||||
v.sort(key=lambda idx: positions[idx].line_price_gross)
|
||||
v.sort(key=lambda idx: positions[idx][2])
|
||||
subevent_order = sorted(list(subevent_to_idx.keys()), key=lambda s: len(subevent_to_idx[s]), reverse=True)
|
||||
|
||||
# Build groups of exactly condition_min_count distinct subevents
|
||||
@@ -431,7 +394,7 @@ class Discount(LoggedModel):
|
||||
l = [ll for ll in l if ll in condition_candidates and ll not in current_group]
|
||||
if cardinality and len(l) != cardinality:
|
||||
continue
|
||||
if se not in {positions[idx].subevent_id for idx in current_group}:
|
||||
if se not in {positions[idx][1] for idx in current_group}:
|
||||
candidates += l
|
||||
cardinality = len(l)
|
||||
|
||||
@@ -440,7 +403,7 @@ class Discount(LoggedModel):
|
||||
|
||||
# Sort the list by prices, then pick one. For "buy 2 get 1 free" we apply a "pick 1 from the start
|
||||
# and 2 from the end" scheme to optimize price distribution among groups
|
||||
candidates = sorted(candidates, key=lambda idx: positions[idx].line_price_gross)
|
||||
candidates = sorted(candidates, key=lambda idx: positions[idx][2])
|
||||
if len(current_group) < (self.benefit_only_apply_to_cheapest_n_matches or 0):
|
||||
candidate = candidates[0]
|
||||
else:
|
||||
@@ -452,14 +415,14 @@ class Discount(LoggedModel):
|
||||
if len(current_group) >= max(self.condition_min_count, 1):
|
||||
candidate_groups.append(current_group)
|
||||
for c in current_group:
|
||||
subevent_to_idx[positions[c].subevent_id].remove(c)
|
||||
subevent_to_idx[positions[c][1]].remove(c)
|
||||
current_group = []
|
||||
|
||||
# Distribute "leftovers"
|
||||
for se in subevent_order:
|
||||
if subevent_to_idx[se]:
|
||||
for group in candidate_groups:
|
||||
if se not in {positions[idx].subevent_id for idx in group}:
|
||||
if se not in {positions[idx][1] for idx in group}:
|
||||
group.append(subevent_to_idx[se].pop())
|
||||
if not subevent_to_idx[se]:
|
||||
break
|
||||
@@ -469,8 +432,6 @@ class Discount(LoggedModel):
|
||||
positions,
|
||||
[idx for idx in g if idx in condition_candidates],
|
||||
[idx for idx in g if idx in benefit_candidates],
|
||||
result,
|
||||
None,
|
||||
None
|
||||
result
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -870,12 +870,10 @@ class Event(EventMixin, LoggedModel):
|
||||
for i in Item.objects.filter(event=other).prefetch_related(
|
||||
'variations', 'limit_sales_channels', 'require_membership_types',
|
||||
'variations__limit_sales_channels', 'variations__require_membership_types',
|
||||
'matched_by_cross_selling_categories',
|
||||
):
|
||||
vars = list(i.variations.all())
|
||||
require_membership_types = list(i.require_membership_types.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
|
||||
i.pk = None
|
||||
i.event = self
|
||||
@@ -913,9 +911,6 @@ class Event(EventMixin, LoggedModel):
|
||||
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]))
|
||||
|
||||
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):
|
||||
i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
|
||||
i.save()
|
||||
|
||||
@@ -63,13 +63,14 @@ from django_countries.fields import Country
|
||||
from django_scopes import ScopedManager
|
||||
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.fields import MultiStringField
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
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):
|
||||
@@ -110,33 +111,6 @@ class ItemCategory(LoggedModel):
|
||||
'only be bought in combination with a product that has this category configured as a possible '
|
||||
'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:
|
||||
verbose_name = _("Product category")
|
||||
@@ -145,31 +119,19 @@ class ItemCategory(LoggedModel):
|
||||
|
||||
def __str__(self):
|
||||
name = self.internal_name or self.name
|
||||
if self.category_type != 'normal':
|
||||
return _('{category} ({category_type})').format(category=str(name),
|
||||
category_type=self.get_category_type_display())
|
||||
if self.is_addon:
|
||||
return _('{category} (Add-On products)').format(category=str(name))
|
||||
return str(name)
|
||||
|
||||
def get_category_type_display(self):
|
||||
if self.is_addon:
|
||||
return _('Add-on category')
|
||||
elif self.cross_selling_mode:
|
||||
return self.get_cross_selling_mode_display()
|
||||
return _('Add-On products')
|
||||
else:
|
||||
return _('Normal category')
|
||||
return None
|
||||
|
||||
@property
|
||||
def category_type(self):
|
||||
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
|
||||
return 'addon' if self.is_addon else 'normal'
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
@@ -308,7 +270,7 @@ class SubEventItemVariation(models.Model):
|
||||
return True
|
||||
|
||||
|
||||
def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
|
||||
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
||||
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
|
||||
# makes the query SIGNIFICANTLY faster
|
||||
from .organizer import SalesChannel
|
||||
@@ -329,8 +291,6 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_
|
||||
|
||||
if not allow_addons:
|
||||
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.item_id:
|
||||
@@ -344,8 +304,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_
|
||||
|
||||
|
||||
class ItemQuerySet(models.QuerySet):
|
||||
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
|
||||
return filter_available(self, channel, voucher, allow_addons, allow_cross_sell)
|
||||
def filter_available(self, channel='web', voucher=None, allow_addons=False):
|
||||
return filter_available(self, channel, voucher, allow_addons)
|
||||
|
||||
|
||||
class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__):
|
||||
@@ -353,8 +313,8 @@ class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__)
|
||||
super().__init__()
|
||||
self._queryset_class = ItemQuerySet
|
||||
|
||||
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, allow_cross_sell)
|
||||
def filter_available(self, channel='web', voucher=None, allow_addons=False):
|
||||
return filter_available(self.get_queryset(), channel, voucher, allow_addons)
|
||||
|
||||
|
||||
class Item(LoggedModel):
|
||||
@@ -1118,12 +1078,13 @@ class ItemVariation(models.Model):
|
||||
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
|
||||
:type original_price: decimal.Decimal
|
||||
: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
|
||||
: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
|
||||
:param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on.
|
||||
:type limit_sales_channels: list
|
||||
|
||||
"""
|
||||
item = models.ForeignKey(
|
||||
Item,
|
||||
|
||||
@@ -3391,74 +3391,6 @@ class BlockedTicketSecret(models.Model):
|
||||
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)
|
||||
def cachedticket_delete(sender, instance, **kwargs):
|
||||
if instance.file:
|
||||
|
||||
@@ -53,30 +53,6 @@ class SeatingPlanLayoutValidator:
|
||||
e = str(e).replace('%', '%%')
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -304,21 +304,10 @@ class TaxRule(LoggedModel):
|
||||
subtract_from_gross = Decimal('0.00')
|
||||
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'):
|
||||
gross = _limit_subtract(base_price, subtract_from_gross)
|
||||
return TaxedPrice(
|
||||
net=gross,
|
||||
gross=gross,
|
||||
net=max(Decimal('0.00'), base_price - subtract_from_gross),
|
||||
gross=max(Decimal('0.00'), base_price - subtract_from_gross),
|
||||
tax=Decimal('0.00'),
|
||||
rate=rate,
|
||||
name=self.name,
|
||||
@@ -331,14 +320,19 @@ class TaxRule(LoggedModel):
|
||||
base_price_is = 'net'
|
||||
|
||||
if base_price_is == 'gross':
|
||||
gross = _limit_subtract(base_price, subtract_from_gross)
|
||||
if base_price >= Decimal('0.00'):
|
||||
# For positive prices, make sure they don't go negative because of bundles
|
||||
gross = max(Decimal('0.00'), base_price - subtract_from_gross)
|
||||
else:
|
||||
# If the price is already negative, we don't really care any more
|
||||
gross = base_price - subtract_from_gross
|
||||
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
|
||||
currency)
|
||||
elif base_price_is == 'net':
|
||||
net = base_price
|
||||
gross = round_decimal((net * (1 + rate / 100)), currency)
|
||||
if subtract_from_gross:
|
||||
gross = _limit_subtract(gross, subtract_from_gross)
|
||||
gross -= subtract_from_gross
|
||||
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
|
||||
currency)
|
||||
else:
|
||||
|
||||
@@ -1542,9 +1542,10 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
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 voucher: A voucher code
|
||||
:param cart_id: The cart ID of the cart to modify
|
||||
:param session: Session ID of a guest
|
||||
"""
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
@@ -1565,10 +1566,10 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||
"""
|
||||
Removes an item specified by its position ID from a user's cart.
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
:param position: A cart position ID
|
||||
:param cart_id: The cart ID of the cart to modify
|
||||
:param session: Session ID of a guest
|
||||
"""
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
@@ -1589,9 +1590,9 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||
"""
|
||||
Removes all items from a user's cart.
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
:param cart_id: The cart ID of the cart to modify
|
||||
:param session: Session ID of a guest
|
||||
"""
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
@@ -1610,15 +1611,13 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: List[dict], cart_id: str=None, locale='en',
|
||||
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
|
||||
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||
"""
|
||||
Assigns addons to eligible products in a user's cart, adding and removing the addon products as necessary to
|
||||
ensure the requested addon state.
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
:param addons: A list of dicts with the keys addon_to, item, variation
|
||||
:param add_to_cart_items: A list of dicts with the keys item, variation, count, custom_price, voucher, seat ID
|
||||
:param cart_id: The cart ID of the cart to modify
|
||||
:param session: Session ID of a guest
|
||||
"""
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
ia = False
|
||||
@@ -1636,7 +1635,6 @@ def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: L
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
|
||||
cm.set_addons(addons)
|
||||
cm.add_new_items(add_to_cart_items)
|
||||
cm.commit()
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from collections import defaultdict
|
||||
from 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
|
||||
@@ -20,9 +20,8 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
@@ -32,7 +31,6 @@ from pretix.base.models import (
|
||||
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation,
|
||||
SalesChannel, Voucher,
|
||||
)
|
||||
from pretix.base.models.discount import Discount, PositionInfo
|
||||
from pretix.base.models.event import Event, SubEvent
|
||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
@@ -157,22 +155,14 @@ def get_line_price(price_after_voucher: Decimal, custom_price_input: Decimal, cu
|
||||
return price
|
||||
|
||||
|
||||
def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
|
||||
positions: List[Tuple[int, Optional[int], Decimal, bool, bool, Decimal]],
|
||||
collect_potential_discounts: Optional[defaultdict]=None) -> List[Tuple[Decimal, Optional[Discount]]]:
|
||||
def apply_discounts(event: Event, sales_channel: str,
|
||||
positions: List[Tuple[int, Optional[int], Decimal, bool, bool]]) -> List[Decimal]:
|
||||
"""
|
||||
Applies any dynamic discounts to a cart
|
||||
|
||||
:param event: Event the cart belongs to
|
||||
: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 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
|
||||
"""
|
||||
if isinstance(sales_channel, SalesChannel):
|
||||
@@ -187,10 +177,10 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
|
||||
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
|
||||
for discount in discount_qs:
|
||||
result = discount.apply({
|
||||
idx: PositionInfo(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)
|
||||
idx: (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)
|
||||
if not is_bundled and idx not in new_prices
|
||||
}, collect_potential_discounts)
|
||||
})
|
||||
for k in result.keys():
|
||||
result[k] = (result[k], discount)
|
||||
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')
|
||||
if v.comment:
|
||||
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(
|
||||
'pretix.voucher.sent',
|
||||
user=user,
|
||||
|
||||
@@ -571,7 +571,7 @@ DEFAULTS = {
|
||||
'form_class': I18nFormField,
|
||||
'serializer_class': I18nField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Custom recipient field label"),
|
||||
label=_("Custom recipient field"),
|
||||
widget=I18nTextInput,
|
||||
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 "
|
||||
@@ -580,18 +580,6 @@ DEFAULTS = {
|
||||
"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': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
|
||||
@@ -367,7 +367,7 @@ validate_cart_addons = EventPluginSignal()
|
||||
Arguments: ``addons``, ``base_position``, ``iao``
|
||||
|
||||
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
|
||||
as the ``ItemAddOn`` object as the argument ``iao`` and the base cart position as
|
||||
``base_position``.
|
||||
@@ -838,12 +838,3 @@ is given as the first argument.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -36,7 +36,6 @@ import sys
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Q
|
||||
from django.urls import Resolver404, get_script_prefix, resolve
|
||||
from django.utils.translation import get_language
|
||||
@@ -153,8 +152,6 @@ def _default_context(request):
|
||||
ctx['warning_update_available'] = True
|
||||
if not gs.settings.update_check_ack and 'runserver' not in sys.argv:
|
||||
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', '')
|
||||
|
||||
|
||||
@@ -844,7 +844,6 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
'invoice_address_company_required',
|
||||
'invoice_address_beneficiary',
|
||||
'invoice_address_custom_field',
|
||||
'invoice_address_custom_field_helptext',
|
||||
'invoice_name_required',
|
||||
'invoice_address_not_asked_free',
|
||||
'invoice_include_free',
|
||||
|
||||
@@ -40,8 +40,7 @@ from urllib.parse import urlencode
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Max, Q
|
||||
from django.forms import ChoiceField, RadioSelect
|
||||
from django.db.models import Max
|
||||
from django.forms.formsets import DELETION_FIELD_NAME
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
@@ -80,67 +79,11 @@ class CategoryForm(I18nModelForm):
|
||||
'name',
|
||||
'internal_name',
|
||||
'description',
|
||||
'cross_selling_condition',
|
||||
'cross_selling_match_products',
|
||||
'is_addon'
|
||||
]
|
||||
widgets = {
|
||||
'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):
|
||||
|
||||
@@ -51,7 +51,6 @@ from pretix.base.models import (
|
||||
Checkin, CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
|
||||
TaxRule,
|
||||
)
|
||||
from pretix.base.models.orders import PrintLog
|
||||
from pretix.base.signals import logentry_display, orderposition_blocked_display
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
@@ -640,16 +639,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
if sender and logentry.action_type.startswith('pretix.event.checkin'):
|
||||
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':
|
||||
# deprecated
|
||||
dt = dateutil.parser.parse(data.get('datetime'))
|
||||
|
||||
@@ -421,14 +421,6 @@
|
||||
</a>
|
||||
</div>
|
||||
{% 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 %}
|
||||
<div class="alert alert-danger">
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
{% bootstrap_field form.invoice_address_beneficiary 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_helptext layout="control" %}
|
||||
{% bootstrap_field form.invoice_address_explanation_text layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
||||
@@ -12,22 +12,10 @@
|
||||
{% endif %}
|
||||
{% if object.id and not object.quotas.exists %}
|
||||
<div class="alert alert-warning">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
{% blocktrans trimmed %}
|
||||
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>
|
||||
{% blocktrans trimmed %}
|
||||
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>
|
||||
{% elif object.pk and not object.is_available_by_time %}
|
||||
<div class="alert alert-warning">
|
||||
|
||||
@@ -67,22 +67,10 @@
|
||||
<div class="panel-body form-horizontal">
|
||||
{% if form.instance.pk and not form.instance.quotas.exists %}
|
||||
<div class="alert alert-warning">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
{% blocktrans trimmed %}
|
||||
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>
|
||||
{% blocktrans trimmed %}
|
||||
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>
|
||||
{% endif %}
|
||||
{% bootstrap_form_errors form %}
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Product categories" %}</th>
|
||||
<th>{% trans "Category type" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -41,9 +40,6 @@
|
||||
<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>
|
||||
</td>
|
||||
<td>
|
||||
{{ c.get_category_type_display }}
|
||||
</td>
|
||||
<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 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,21 +16,7 @@
|
||||
{% bootstrap_field form.internal_name layout="control" %}
|
||||
</div>
|
||||
{% bootstrap_field form.description 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>
|
||||
<div data-display-dependency="#id_category_type_2,#id_category_type_3">
|
||||
{% bootstrap_field form.cross_selling_condition layout="control" horizontal_field_class="col-md-9" %}
|
||||
{% bootstrap_field form.cross_selling_match_products layout="control" %}
|
||||
</div>
|
||||
{% bootstrap_field form.is_addon layout="control" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% if category %}
|
||||
|
||||
@@ -13,52 +13,31 @@
|
||||
{% trans "Edit question" %}
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Filter" %}</h3>
|
||||
</div>
|
||||
<form class="panel-body filter-form" action="" method="get">
|
||||
<div class="row">
|
||||
<div class="col-lg-2 col-sm-6 col-xs-6">
|
||||
<select name="status" class="form-control">
|
||||
<option value="" {% if request.GET.status == "" %}selected="selected"{% endif %}>{% trans "All orders" %}</option>
|
||||
<option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
|
||||
<option value="pv" {% if request.GET.status == "pv" %}selected="selected"{% endif %}>{% trans "Paid or confirmed" %}</option>
|
||||
<option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
|
||||
<option value="np" {% if request.GET.status == "np" or "status" not in request.GET %}selected="selected"{% endif %}>{% trans "Pending or paid" %}</option>
|
||||
<option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>
|
||||
<option value="e" {% if request.GET.status == "e" %}selected="selected"{% endif %}>{% trans "Expired" %}</option>
|
||||
<option value="ne" {% if request.GET.status == "ne" %}selected="selected"{% endif %}>{% trans "Pending or expired" %}</option>
|
||||
<option value="c" {% if request.GET.status == "c" %}selected="selected"{% endif %}>{% trans "Canceled" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-5 col-sm-6 col-xs-6">
|
||||
<select name="item" class="form-control">
|
||||
<option value="">{% trans "All products" %}</option>
|
||||
{% for item in items %}
|
||||
<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>
|
||||
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
<p>
|
||||
<select name="status" class="form-control">
|
||||
<option value="" {% if request.GET.status == "" %}selected="selected"{% endif %}>{% trans "All orders" %}</option>
|
||||
<option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
|
||||
<option value="pv" {% if request.GET.status == "pv" %}selected="selected"{% endif %}>{% trans "Paid or confirmed" %}</option>
|
||||
<option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
|
||||
<option value="np" {% if request.GET.status == "np" or "status" not in request.GET %}selected="selected"{% endif %}>{% trans "Pending or paid" %}</option>
|
||||
<option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>
|
||||
<option value="e" {% if request.GET.status == "e" %}selected="selected"{% endif %}>{% trans "Expired" %}</option>
|
||||
<option value="ne" {% if request.GET.status == "ne" %}selected="selected"{% endif %}>{% trans "Pending or expired" %}</option>
|
||||
<option value="c" {% if request.GET.status == "c" %}selected="selected"{% endif %}>{% trans "Canceled" %}</option>
|
||||
</select>
|
||||
<select name="item" class="form-control">
|
||||
<option value="">{% trans "All products" %}</option>
|
||||
{% for item in items %}
|
||||
<option value="{{ item.id }}"
|
||||
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
|
||||
{{ item.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
</p>
|
||||
</form>
|
||||
<div class="row" id="question-stats">
|
||||
{% if not stats %}
|
||||
<div class="empty-collection col-md-10 col-xs-12">
|
||||
@@ -96,7 +75,7 @@
|
||||
{% for stat in stats %}
|
||||
<tr>
|
||||
<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 }}&subevent={{ request.GET.subevent }}&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 }}&question={{ question.pk }}&answer={{ stat.alink|default:stat.answer|urlencode }}">
|
||||
{{ stat.answer }}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{% 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." %}
|
||||
{% trans "According to your event settings, sold out products are hidden from customers. This way, customers will not be able to discovere the waiting list." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
|
||||
@@ -94,9 +94,7 @@ def process_login(request, user, keep_logged_in):
|
||||
pretix_successful_logins.inc(1)
|
||||
handle_login_source(user, request)
|
||||
auth_login(request, user)
|
||||
t = int(time.time())
|
||||
request.session['pretix_auth_login_time'] = t
|
||||
request.session['pretix_auth_last_used'] = t
|
||||
request.session['pretix_auth_login_time'] = int(time.time())
|
||||
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
|
||||
return redirect_to_url(next_url)
|
||||
return redirect('control:index')
|
||||
|
||||
@@ -39,7 +39,7 @@ from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.db.models import Exists, Max, OuterRef, Prefetch, Q, Subquery
|
||||
from django.http import Http404, HttpResponseNotAllowed, HttpResponseRedirect
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
@@ -193,9 +193,6 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
|
||||
class CheckInListBulkRevertConfirmView(CheckInListQueryMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
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):
|
||||
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@@ -302,8 +302,6 @@ class CategoryCreate(EventPermissionRequiredMixin, CreateView):
|
||||
i = modelcopy(self.copy_from)
|
||||
i.pk = None
|
||||
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:
|
||||
kwargs['instance'] = ItemCategory(event=self.request.event)
|
||||
return kwargs
|
||||
@@ -663,10 +661,6 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV
|
||||
question=self.object, orderposition__isnull=False,
|
||||
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")
|
||||
if s != "":
|
||||
if s == 'o':
|
||||
@@ -920,19 +914,16 @@ class QuotaCreate(EventPermissionRequiredMixin, CreateView):
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
|
||||
kwargs.setdefault('initial', {})
|
||||
if self.copy_from:
|
||||
i = modelcopy(self.copy_from)
|
||||
i.pk = None
|
||||
kwargs['instance'] = i
|
||||
kwargs.setdefault('initial', {})
|
||||
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()
|
||||
]
|
||||
else:
|
||||
kwargs['instance'] = Quota(event=self.request.event)
|
||||
if 'product' in self.request.GET:
|
||||
kwargs['initial']['itemvars'] = self.request.GET.getlist('product')
|
||||
|
||||
return kwargs
|
||||
|
||||
def form_invalid(self, form):
|
||||
|
||||
@@ -156,7 +156,7 @@ def event_list(request):
|
||||
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from'))
|
||||
).annotate(
|
||||
order_from=Coalesce('min_from', 'date_from'),
|
||||
).order_by('-order_from', 'slug')
|
||||
).order_by('-order_from')
|
||||
|
||||
total = qs.count()
|
||||
pagesize = 20
|
||||
@@ -318,7 +318,7 @@ def nav_context_list(request):
|
||||
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from'))
|
||||
).annotate(
|
||||
order_from=Coalesce('min_from', 'date_from'),
|
||||
).order_by('-order_from', 'slug')
|
||||
).order_by('-order_from')
|
||||
|
||||
if request.user.has_active_staff_session(request.session.session_key):
|
||||
qs_orga = Organizer.objects.all()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -622,40 +622,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -656,40 +656,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr "تباين اللون سيئ للخلفية البيضاء، الرجاء اختيار لون غامق."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr "البحث في الاستفسارات"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr "الكل"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr "لا شيء"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr "المختارة فقط"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr "قم باستخدم اسم مختلف داخليا"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr "اضغط لاغلاق الصفحة"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "لم تقم بحفظ التعديلات!"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -623,40 +623,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: 2023-09-15 06:00+0000\n"
|
||||
"Last-Translator: Michael <michael.happl@gmx.at>\n"
|
||||
"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 "
|
||||
"tmavší odstín."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr "Hledaný výraz"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr "Všechny"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr "Žádný"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr "Pouze vybrané"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Interně používat jiný název"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr "Kliknutím zavřete"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Máte neuložené změny!"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -623,40 +623,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: 2024-07-10 15:00+0000\n"
|
||||
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -670,40 +670,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr "Ingen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr "Klik for at lukke"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Du har ændringer, der ikke er gemt!"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: 2024-09-10 07:17+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -644,40 +644,40 @@ msgstr ""
|
||||
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
|
||||
"Hintergrund. Bitte wählen Sie eine dunklere Farbe."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr "Suchbegriff"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr "Keine"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr "Nur ausgewählte"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr "Geben Sie eine Seitenzahl zwischen 1 und %(max)s ein."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr "Ungültige Seitenzahl."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Intern einen anderen Namen verwenden"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr "Klicken zum Schließen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Sie haben ungespeicherte Änderungen!"
|
||||
|
||||
|
||||
@@ -82,7 +82,6 @@ Connect
|
||||
Consent
|
||||
Copyleft
|
||||
Cronjob
|
||||
Cross
|
||||
csv
|
||||
Customer
|
||||
CZK
|
||||
@@ -281,7 +280,6 @@ Scopes
|
||||
sechsstelligen
|
||||
Secret
|
||||
Security
|
||||
Selling
|
||||
SEPA
|
||||
Shirts
|
||||
Signaturverfahren
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: 2024-09-10 07:17+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
@@ -643,40 +643,40 @@ msgstr ""
|
||||
"Diese Farbe hat einen schlechten Kontrast für Text auf einem weißen "
|
||||
"Hintergrund. Bitte wähle eine dunklere Farbe."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr "Suchbegriff"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr "Keine"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr "Nur ausgewählte"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr "Gib eine Seitenzahl zwischen 1 und %(max)s ein."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr "Ungültige Seitenzahl."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Intern einen anderen Namen verwenden"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr "Klicken zum Schließen"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Du hast ungespeicherte Änderungen!"
|
||||
|
||||
|
||||
@@ -82,7 +82,6 @@ Connect
|
||||
Consent
|
||||
Copyleft
|
||||
Cronjob
|
||||
Cross
|
||||
csv
|
||||
Customer
|
||||
CZK
|
||||
@@ -281,7 +280,6 @@ Scopes
|
||||
sechsstelligen
|
||||
Secret
|
||||
Security
|
||||
Selling
|
||||
SEPA
|
||||
Shirts
|
||||
Signaturverfahren
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-29 08:54+0000\n"
|
||||
"POT-Creation-Date: 2024-09-26 11:23+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -622,40 +622,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
|
||||
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -686,40 +686,40 @@ msgstr ""
|
||||
"Το χρώμα σας έχει κακή αντίθεση για κείμενο σε λευκό φόντο, επιλέξτε μια πιο "
|
||||
"σκούρα σκιά."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr "Όλα"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr "Κανένας"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr "Κάντε κλικ για να κλείσετε"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -622,40 +622,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,17 +7,17 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"PO-Revision-Date: 2024-10-22 17:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix-js/es/>\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: 2024-07-19 08:56+0000\n"
|
||||
"Last-Translator: Reece Needham <nouveaureece@protonmail.com>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/es/>\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.7.2\n"
|
||||
"X-Generator: Weblate 5.6.2\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -576,8 +576,10 @@ msgid "Text object (deprecated)"
|
||||
msgstr "Objeto de texto"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:901
|
||||
#, fuzzy
|
||||
#| msgid "Text object"
|
||||
msgid "Text box"
|
||||
msgstr "Campo de texto"
|
||||
msgstr "Objeto de texto"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:903
|
||||
msgid "Barcode area"
|
||||
@@ -644,40 +646,40 @@ msgstr ""
|
||||
"Tu color tiene mal contraste para un texto con fondo blanco, por favor, "
|
||||
"escoge un tono más oscuro."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr "Consultar búsqueda"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr "Todos"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr "Ninguno"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr "Solamente seleccionados"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr "Introduce un número de página entre 1 y %(max)s."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr "Número de página inválido."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Usar un nombre diferente internamente"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr "Click para cerrar"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "¡Tienes cambios sin guardar!"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-09-26 11:23+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -622,40 +622,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-09-26 11:23+0000\n"
|
||||
"PO-Revision-Date: 2024-09-06 08:47+0000\n"
|
||||
"Last-Translator: Albizuri <oier@puntu.eus>\n"
|
||||
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -635,40 +635,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr "Guztiak"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
|
||||
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
@@ -651,40 +651,40 @@ msgid ""
|
||||
"darker shade."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr "Kaikki"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
msgid "Invalid page number."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Käytä toista nimeä sisäisesti"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr "Sulje klikkaamalla"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Sinulla on tallentamattomia muutoksia!"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: French\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
|
||||
"PO-Revision-Date: 2024-06-12 03:00+0000\n"
|
||||
"Last-Translator: simonD <simon.dalvy@gmail.com>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
@@ -656,41 +656,41 @@ msgstr ""
|
||||
"Votre choix de couleur n'a pas un bon contraste avec du texte sur un fond "
|
||||
"blanc, SVP choisissez un ton plus sombre."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:495
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:515
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:491
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:511
|
||||
msgid "Search query"
|
||||
msgstr "Recherche"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:513
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:509
|
||||
msgid "All"
|
||||
msgstr "Tous"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:510
|
||||
msgid "None"
|
||||
msgstr "Aucun"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:518
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:514
|
||||
msgid "Selected only"
|
||||
msgstr "Seuls les sélectionnés"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:861
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:847
|
||||
msgid "Enter page number between 1 and %(max)s."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:864
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:850
|
||||
#, fuzzy
|
||||
msgid "Invalid page number."
|
||||
msgstr "Numéro de page invalide."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1022
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1008
|
||||
msgid "Use a different name internally"
|
||||
msgstr "Utiliser un nom différent en interne"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1062
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1048
|
||||
msgid "Click to close"
|
||||
msgstr "Cliquez pour fermer"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1137
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:1123
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr "Vous avez des modifications non sauvegardées !"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user