forked from CGM_Public/pretix_original
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10bc49bc3a | |||
| 5aba1f9a23 | |||
| a4eed87396 | |||
| 08879d0d55 | |||
| c276a19bcc | |||
| 1e3c6e0b68 | |||
| 4e283eb560 | |||
| 52a1983630 | |||
| 3d85d9d865 | |||
| 4ca9a43890 | |||
| d8bac7db65 | |||
| 91de0f93e6 | |||
| 901565203b | |||
| 14c6c9c0d7 | |||
| 6de6cf6c08 | |||
| 29306b3a4d | |||
| ca69996611 | |||
| 16419b6ae4 | |||
| d6258b9b54 | |||
| 6f75608196 | |||
| 6ef88e009b | |||
| 957100a195 | |||
| 112ef0908f | |||
| 91aaff7359 | |||
| 8ab61e2c38 | |||
| c8ba5cc427 | |||
| 5ebad31b7d | |||
| 0429377f7d | |||
| 76e4b797a1 | |||
| 5f0009c996 | |||
| de63a4be01 | |||
| f3432139cb | |||
| 0b82ac9115 | |||
| eb685b5141 | |||
| 5f7f0bd8f1 | |||
| 9fcef2dcaa | |||
| fc3b186b93 | |||
| a406884575 | |||
| 57ccd5f289 | |||
| f4ac7e7f65 | |||
| 81d7045b31 | |||
| f9502a3212 | |||
| a31f624417 | |||
| 3f99e0bece | |||
| 7e64f2b38a | |||
| ee2bc93608 | |||
| fb4bed9d0d | |||
| aec75e4d0c | |||
| e7e41470fb | |||
| 0aa9dda90a | |||
| d97c983b6f | |||
| 6c957f31ca | |||
| 8e6b4b3ec7 | |||
| b24de62f73 | |||
| cdbd220a12 | |||
| 2f11aee512 | |||
| 8ea475ce39 | |||
| b29bc9db96 | |||
| 6bd6694132 | |||
| 110e6e248e | |||
| 985f4d969d | |||
| 826bd07b01 | |||
| 3e4e86742a | |||
| ef5fcde5d9 | |||
| 8f1d53d016 | |||
| 9ca1573fcf | |||
| 5795aa6492 | |||
| 6e0613a2af | |||
| b43ed38483 | |||
| f0fedf0001 | |||
| 19373b8f91 | |||
| 45fd13786a | |||
| ae5111ee7e | |||
| d8bf3065d0 | |||
| 54f077665c | |||
| 482a66c546 | |||
| e4cef6e46b | |||
| cbee1b71fe | |||
| 0cd1290624 | |||
| 565f5e2ea7 | |||
| b46c0eba0c |
@@ -38,7 +38,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system dependencies
|
||||
run: sudo apt update && sudo apt install gettext unzip
|
||||
run: sudo apt update && sudo apt install -y gettext unzip
|
||||
- name: Install Python dependencies
|
||||
run: pip3 install -U setuptools build pip check-manifest
|
||||
- name: Run check-manifest
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install enchant-2 hunspell aspell-en
|
||||
run: sudo apt update && sudo apt install -y enchant-2 hunspell aspell-en
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -Ur requirements.txt
|
||||
working-directory: ./doc
|
||||
|
||||
@@ -35,9 +35,9 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system packages
|
||||
run: sudo apt update && sudo apt install gettext
|
||||
run: sudo apt update && sudo apt -y install gettext
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -e ".[dev]"
|
||||
run: pip3 install uv && uv pip install --system -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 -e ".[dev]"
|
||||
run: pip3 install uv && uv pip install --system -e ".[dev]"
|
||||
- name: Spellcheck translations
|
||||
run: potypo
|
||||
working-directory: ./src
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -e ".[dev]" psycopg2-binary
|
||||
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||
- name: Run isort
|
||||
run: isort -c .
|
||||
working-directory: ./src
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install Dependencies
|
||||
run: pip3 install -e ".[dev]" psycopg2-binary
|
||||
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||
- name: Run flake8
|
||||
run: flake8 .
|
||||
working-directory: ./src
|
||||
|
||||
+19
-13
@@ -30,15 +30,21 @@ 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:
|
||||
@@ -50,9 +56,9 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Install system dependencies
|
||||
run: sudo apt update && sudo apt install gettext
|
||||
run: sudo apt update && sudo apt install -y gettext
|
||||
- name: Install Python dependencies
|
||||
run: pip3 install --ignore-requires-python -e ".[dev]" psycopg2-binary # We ignore that flake8 needs newer python as we don't run flake8 during tests
|
||||
run: pip3 install uv && uv pip install --system -e ".[dev]" psycopg2-binary
|
||||
- name: Run checks
|
||||
run: python manage.py check
|
||||
working-directory: ./src
|
||||
@@ -64,15 +70,15 @@ jobs:
|
||||
run: make all compress
|
||||
- name: Run tests
|
||||
working-directory: ./src
|
||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
|
||||
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml tests --maxfail=100
|
||||
- name: Run concurrency tests
|
||||
working-directory: ./src
|
||||
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
|
||||
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reuse-db
|
||||
if: matrix.database == 'postgres'
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: src/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
fail_ci_if_error: false
|
||||
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ tests:
|
||||
- cd src
|
||||
- python manage.py check
|
||||
- make all compress
|
||||
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
|
||||
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
|
||||
except:
|
||||
- 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,
|
||||
|
||||
@@ -31,8 +31,6 @@ subevent integer ID of the date
|
||||
position_count integer Number of tickets that match this list (read-only).
|
||||
checkin_count integer Number of check-ins performed on this list (read-only).
|
||||
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
||||
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
|
||||
**Deprecated, will be removed in pretix 2024.10.** Use :ref:`rest-autocheckinrules`: instead.
|
||||
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
|
||||
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||
@@ -91,10 +89,7 @@ Endpoints
|
||||
"allow_entry_after_exit": true,
|
||||
"exit_all_at": null,
|
||||
"rules": {},
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
"addon_match": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -146,10 +141,7 @@ Endpoints
|
||||
"allow_entry_after_exit": true,
|
||||
"exit_all_at": null,
|
||||
"rules": {},
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
"addon_match": false
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -246,10 +238,7 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
"addon_match": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -271,10 +260,7 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
"addon_match": false
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a list for
|
||||
@@ -326,10 +312,7 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
"addon_match": false
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
@@ -342,7 +325,7 @@ Endpoints
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/checkinlist/(id)/
|
||||
|
||||
Delete a check-in list. Note that this also deletes the information on all check-ins performed via this list.
|
||||
Delete a check-in list. **Note that this also deletes the information on all check-ins performed via this list.**
|
||||
|
||||
**Example request**:
|
||||
|
||||
|
||||
@@ -104,6 +104,10 @@ url string The full URL to
|
||||
payments list of objects List of payment processes (see below)
|
||||
refunds list of objects List of refund processes (see below)
|
||||
last_modified datetime Last modification of this object
|
||||
cancellation_date datetime Time of order cancellation (or ``null``). **Note**:
|
||||
Will not be set for partial cancellations and is not
|
||||
reliable for orders that have been cancelled,
|
||||
reactivated and cancelled again.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -151,6 +155,9 @@ last_modified datetime Last modificati
|
||||
|
||||
The ``expires`` attribute can now be passed during order creation.
|
||||
|
||||
.. versionchanged:: 2024.11
|
||||
|
||||
The ``cancellation_date`` attribute has been added and can also be used as an ordering key.
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
@@ -206,6 +213,17 @@ checkins list of objects List of **succe
|
||||
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
|
||||
├ device_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
|
||||
@@ -233,6 +251,10 @@ 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
|
||||
@@ -399,10 +421,21 @@ 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,
|
||||
@@ -438,14 +471,15 @@ List of all orders
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
],
|
||||
"refunds": []
|
||||
"refunds": [],
|
||||
"cancellation_date": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``code``,
|
||||
``last_modified``, and ``status``. Default: ``datetime``
|
||||
``last_modified``, ``status`` and ``cancellation_date``. Default: ``datetime``
|
||||
:query string code: Only return orders that match the given order code
|
||||
:query string status: Only return orders in the given order status (see above)
|
||||
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
|
||||
@@ -626,10 +660,22 @@ 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,
|
||||
@@ -665,7 +711,8 @@ Fetching individual orders
|
||||
"provider": "banktransfer"
|
||||
}
|
||||
],
|
||||
"refunds": []
|
||||
"refunds": [],
|
||||
"cancellation_date": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -977,8 +1024,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``
|
||||
|
||||
@@ -1581,10 +1628,22 @@ 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,
|
||||
@@ -1695,10 +1754,22 @@ 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,
|
||||
@@ -1795,6 +1866,10 @@ 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:
|
||||
@@ -2054,6 +2129,59 @@ 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
|
||||
-----------------------
|
||||
|
||||
|
||||
@@ -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-reusablemedia`:
|
||||
.. _`rest-seats`:
|
||||
|
||||
Seats
|
||||
=====
|
||||
|
||||
@@ -17,6 +17,7 @@ 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:
|
||||
|
||||
@@ -22,12 +22,14 @@ 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
|
||||
|
||||
|
||||
@@ -39,18 +41,21 @@ 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
|
||||
@@ -62,24 +67,28 @@ 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
|
||||
@@ -89,4 +98,5 @@ API
|
||||
---
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
:members: validate_event_settings, api_event_settings_fields
|
||||
|
||||
@@ -60,6 +60,7 @@ that we'll provide in this plugin:
|
||||
Similar signals exist for other objects:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
:members: voucher_import_columns
|
||||
|
||||
|
||||
|
||||
@@ -84,8 +84,6 @@ convenient to you:
|
||||
|
||||
.. automethod:: _register_fonts
|
||||
|
||||
.. automethod:: _register_event_fonts
|
||||
|
||||
.. automethod:: _on_first_page
|
||||
|
||||
.. automethod:: _on_other_page
|
||||
|
||||
@@ -86,7 +86,10 @@ 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``s. This is often used in systems, where the same
|
||||
but different ``name`` value. This is often used in systems, where the same
|
||||
information (for example a persons name) is saved in different fields -
|
||||
for example because one institution is returning SAML 1.0 and other
|
||||
institutions are returning SAML 2.0 style attributes. Typically, this only
|
||||
|
||||
+7
-9
@@ -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.6.*",
|
||||
"django-hijack==3.7.*",
|
||||
"django-i18nfield==1.9.*,>=1.9.4",
|
||||
"django-libsass==0.9",
|
||||
"django-localflavor==4.0",
|
||||
@@ -74,26 +74,25 @@ dependencies = [
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.9.*",
|
||||
"phonenumberslite==8.13.*",
|
||||
"Pillow==10.4.*",
|
||||
"Pillow==11.0.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==5.28.*",
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
"pycparser==2.22",
|
||||
"pycryptodome==3.21.*",
|
||||
"pypdf==5.0.*",
|
||||
"pypdf==5.1.*",
|
||||
"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.1.*",
|
||||
"redis==5.2.*",
|
||||
"reportlab==4.2.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==2.17.*",
|
||||
"sentry-sdk==2.18.*",
|
||||
"sepaxml==2.6.*",
|
||||
"slimit",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
"tlds>=2020041600",
|
||||
@@ -102,7 +101,7 @@ dependencies = [
|
||||
"vat_moss_forked==2020.3.20.0.11.0",
|
||||
"vobject==0.9.*",
|
||||
"webauthn==2.2.*",
|
||||
"zeep==4.2.*"
|
||||
"zeep==4.3.*"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -117,12 +116,11 @@ dev = [
|
||||
"isort==5.13.*",
|
||||
"pep8-naming==0.14.*",
|
||||
"potypo",
|
||||
"pytest-asyncio",
|
||||
"pytest-asyncio>=0.24",
|
||||
"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.10.0.dev0"
|
||||
__version__ = "2024.11.0.dev0"
|
||||
|
||||
@@ -77,6 +77,7 @@ 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'),
|
||||
@@ -112,6 +113,7 @@ 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'),
|
||||
@@ -147,6 +149,7 @@ 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'),
|
||||
@@ -188,6 +191,7 @@ 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'),
|
||||
|
||||
@@ -26,31 +26,22 @@ from rest_framework.exceptions import ValidationError
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import Checkin, CheckinList, SalesChannel
|
||||
from pretix.base.models import Checkin, CheckinList
|
||||
|
||||
|
||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
checkin_count = serializers.IntegerField(read_only=True)
|
||||
position_count = serializers.IntegerField(read_only=True)
|
||||
auto_checkin_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||
'include_pending', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
RevokedTicketSecret,
|
||||
PrintLog, RevokedTicketSecret,
|
||||
)
|
||||
from pretix.base.pdf import get_images, get_variables
|
||||
from pretix.base.services.cart import error_messages
|
||||
@@ -284,6 +284,26 @@ class CheckinSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
||||
|
||||
|
||||
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)
|
||||
@@ -476,6 +496,7 @@ 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)
|
||||
@@ -490,7 +511,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',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
||||
read_only_fields = (
|
||||
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
||||
@@ -577,9 +598,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'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')
|
||||
'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')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -732,12 +753,12 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||
'url', 'customer', 'valid_if_pending', 'api_meta'
|
||||
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date'
|
||||
)
|
||||
read_only_fields = (
|
||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
||||
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel'
|
||||
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -1494,6 +1515,7 @@ 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,6 +62,7 @@ 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,
|
||||
)
|
||||
@@ -115,7 +116,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
if 'subevent' in self.request.query_params.getlist('expand'):
|
||||
qs = qs.prefetch_related(
|
||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
||||
'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
|
||||
'subevent__seat_category_mappings', 'subevent__meta_values',
|
||||
)
|
||||
return qs
|
||||
|
||||
@@ -142,7 +143,9 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def perform_destroy(self, instance):
|
||||
instance.checkins.all().delete()
|
||||
instance.log_action(
|
||||
'pretix.event.checkinlist.deleted',
|
||||
user=self.request.user,
|
||||
@@ -365,8 +368,9 @@ 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])
|
||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||
),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||
@@ -378,6 +382,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
||||
'positions',
|
||||
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',
|
||||
)
|
||||
)
|
||||
@@ -389,8 +394,9 @@ 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])
|
||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||
),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import base64
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from cryptography.hazmat.backends.openssl.backend import Backend
|
||||
@@ -146,6 +147,8 @@ 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)
|
||||
|
||||
@@ -160,6 +163,8 @@ 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')
|
||||
@@ -174,6 +179,10 @@ 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)
|
||||
|
||||
@@ -182,9 +191,12 @@ 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')
|
||||
@@ -200,9 +212,8 @@ class UpdateView(APIView):
|
||||
device.save()
|
||||
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
||||
|
||||
from ...base.signals import device_info_updated
|
||||
device_info_updated.send(
|
||||
sender=Device, old_device=request.auth, new_device=device
|
||||
sender=Device, old_device=old_instance, new_device=device
|
||||
)
|
||||
|
||||
serializer = DeviceSerializer(device)
|
||||
|
||||
@@ -42,6 +42,7 @@ 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
|
||||
|
||||
@@ -79,6 +80,7 @@ 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,7 +57,8 @@ from pretix.api.serializers.order import (
|
||||
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
||||
OrderPositionSerializer, OrderRefundCreateSerializer,
|
||||
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
||||
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
|
||||
PrintLogSerializer, RevokedTicketSecretSerializer,
|
||||
SimulatedOrderSerializer,
|
||||
)
|
||||
from pretix.api.serializers.orderchange import (
|
||||
BlockNameSerializer, OrderChangeOperationSerializer,
|
||||
@@ -75,7 +76,7 @@ from pretix.base.models import (
|
||||
TeamAPIToken, generate_secret,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret,
|
||||
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret,
|
||||
)
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.pdf import get_images
|
||||
@@ -214,7 +215,7 @@ class OrderViewSetMixin:
|
||||
queryset = Order.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||
ordering = ('datetime',)
|
||||
ordering_fields = ('datetime', 'code', 'status', 'last_modified')
|
||||
ordering_fields = ('datetime', 'code', 'status', 'last_modified', 'cancellation_date')
|
||||
filterset_class = OrderFilter
|
||||
lookup_field = 'code'
|
||||
|
||||
@@ -259,6 +260,7 @@ 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')
|
||||
)),
|
||||
@@ -280,6 +282,7 @@ 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',
|
||||
@@ -1093,6 +1096,7 @@ 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')
|
||||
@@ -1136,6 +1140,7 @@ 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'
|
||||
@@ -1254,6 +1259,34 @@ 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()
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Generated by Django 4.2.16 on 2024-09-19 10:41
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||
("pretixbase", "0271_itemcategory_cross_selling"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="PrintLog",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("datetime", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("created", models.DateTimeField(auto_now_add=True, null=True)),
|
||||
("successful", models.BooleanField(default=True)),
|
||||
("source", models.CharField(max_length=255)),
|
||||
("type", models.CharField(max_length=255)),
|
||||
("info", models.JSONField(default=dict)),
|
||||
(
|
||||
"api_token",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="pretixbase.teamapitoken",
|
||||
),
|
||||
),
|
||||
(
|
||||
"device",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="print_logs",
|
||||
to="pretixbase.device",
|
||||
),
|
||||
),
|
||||
(
|
||||
"oauth_application",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"position",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="print_logs",
|
||||
to="pretixbase.orderposition",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="print_logs",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ("-datetime",),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 4.2.16 on 2024-10-29 15:03
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_autocheckin(apps, schema_editor):
|
||||
CheckinList = apps.get_model("pretixbase", "CheckinList")
|
||||
AutoCheckinRule = apps.get_model("autocheckin", "AutoCheckinRule")
|
||||
|
||||
for cl in CheckinList.objects.filter(auto_checkin_sales_channels__isnull=False).select_related("event", "event__organizer"):
|
||||
sales_channels = cl.auto_checkin_sales_channels.all()
|
||||
all_sales_channels = cl.event.organizer.sales_channels.all()
|
||||
|
||||
if "pretix.plugins.autocheckin" not in cl.event.plugins:
|
||||
cl.event.plugins = cl.event.plugins + ",pretix.plugins.autocheckin"
|
||||
cl.event.save()
|
||||
|
||||
r = AutoCheckinRule.objects.get_or_create(
|
||||
list=cl,
|
||||
event=cl.event,
|
||||
all_products=True,
|
||||
all_payment_methods=True,
|
||||
defaults=dict(
|
||||
mode="placed",
|
||||
all_sales_channels=len(sales_channels) == len(all_sales_channels),
|
||||
)
|
||||
)[0]
|
||||
if len(sales_channels) != len(all_sales_channels):
|
||||
r.limit_sales_channels.set(sales_channels)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0272_printlog"),
|
||||
("autocheckin", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
migrate_autocheckin,
|
||||
migrations.RunPython.noop,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="checkinlist",
|
||||
name="auto_checkin_sales_channels",
|
||||
),
|
||||
]
|
||||
@@ -99,14 +99,6 @@ class CheckinList(LoggedModel):
|
||||
verbose_name=_('Automatically check out everyone at'),
|
||||
null=True, blank=True
|
||||
)
|
||||
auto_checkin_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_('Sales channels to automatically check in'),
|
||||
help_text=_('This option is deprecated and will be removed in the next months. As a replacement, our new plugin '
|
||||
'"Auto check-in" can be used. When we remove this option, we will automatically migrate your event '
|
||||
'to use the new plugin.'),
|
||||
blank=True,
|
||||
)
|
||||
rules = models.JSONField(default=dict, blank=True)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
@@ -141,7 +133,7 @@ class CheckinList(LoggedModel):
|
||||
return self.positions_query(ignore_status=False)
|
||||
|
||||
@scopes_disabled()
|
||||
def positions_inside_query(self, ignore_status=False, at_time=None):
|
||||
def _filter_positions_inside(self, qs, at_time=None):
|
||||
if at_time is None:
|
||||
c_q = []
|
||||
else:
|
||||
@@ -149,7 +141,7 @@ class CheckinList(LoggedModel):
|
||||
|
||||
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
|
||||
# Use a simple approach that works on all databases
|
||||
qs = self.positions_query(ignore_status=ignore_status).annotate(
|
||||
qs = qs.annotate(
|
||||
last_entry=Subquery(
|
||||
Checkin.objects.filter(
|
||||
*c_q,
|
||||
@@ -202,7 +194,7 @@ class CheckinList(LoggedModel):
|
||||
.values("position_id", "type", "datetime", "cnt_exists_after")
|
||||
.query.sql_with_params()
|
||||
)
|
||||
return self.positions_query(ignore_status=ignore_status).filter(
|
||||
return qs.filter(
|
||||
pk__in=RawSQL(
|
||||
f"""
|
||||
SELECT "position_id"
|
||||
@@ -214,6 +206,10 @@ class CheckinList(LoggedModel):
|
||||
)
|
||||
)
|
||||
|
||||
@scopes_disabled()
|
||||
def positions_inside_query(self, ignore_status=False, at_time=None):
|
||||
return self._filter_positions_inside(self.positions_query(ignore_status=ignore_status), at_time=at_time)
|
||||
|
||||
@property
|
||||
def positions_inside(self):
|
||||
return self.positions_inside_query(None)
|
||||
|
||||
@@ -1024,10 +1024,9 @@ class Event(EventMixin, LoggedModel):
|
||||
|
||||
checkin_list_map = {}
|
||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
|
||||
'limit_products', 'auto_checkin_sales_channels'
|
||||
'limit_products'
|
||||
):
|
||||
items = list(cl.limit_products.all())
|
||||
auto_checkin_sales_channels = list(cl.auto_checkin_sales_channels.all())
|
||||
checkin_list_map[cl.pk] = cl
|
||||
cl.pk = None
|
||||
cl._prefetched_objects_cache = {}
|
||||
@@ -1039,8 +1038,6 @@ class Event(EventMixin, LoggedModel):
|
||||
cl.log_action('pretix.object.cloned')
|
||||
for i in items:
|
||||
cl.limit_products.add(item_map[i.pk])
|
||||
if auto_checkin_sales_channels:
|
||||
cl.auto_checkin_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in auto_checkin_sales_channels]))
|
||||
|
||||
if other.seating_plan:
|
||||
if other.seating_plan.organizer_id == self.organizer_id:
|
||||
|
||||
@@ -1118,13 +1118,12 @@ class ItemVariation(models.Model):
|
||||
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
|
||||
: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,6 +3391,74 @@ 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,6 +53,30 @@ 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):
|
||||
"""
|
||||
|
||||
@@ -343,11 +343,13 @@ class CartManager:
|
||||
err = error_messages['some_subevent_not_started']
|
||||
cp.addons.all().delete()
|
||||
cp.delete()
|
||||
continue
|
||||
|
||||
if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
|
||||
err = error_messages['some_subevent_ended']
|
||||
cp.addons.all().delete()
|
||||
cp.delete()
|
||||
continue
|
||||
|
||||
if cp.subevent:
|
||||
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||
@@ -360,6 +362,7 @@ class CartManager:
|
||||
err = error_messages['some_subevent_ended']
|
||||
cp.addons.all().delete()
|
||||
cp.delete()
|
||||
continue
|
||||
return err
|
||||
|
||||
def _update_subevents_cache(self, se_ids: List[int]):
|
||||
|
||||
@@ -57,7 +57,7 @@ from pretix.base.models import (
|
||||
Checkin, CheckinList, Device, Event, Gate, Item, ItemVariation, Order,
|
||||
OrderPosition, QuestionOption,
|
||||
)
|
||||
from pretix.base.signals import checkin_created, order_placed, periodic_task
|
||||
from pretix.base.signals import checkin_created, periodic_task
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.jsonlogic import Logic
|
||||
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
||||
@@ -1154,23 +1154,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
)
|
||||
|
||||
|
||||
@receiver(order_placed, dispatch_uid="legacy_autocheckin_order_placed")
|
||||
def order_placed(sender, **kwargs):
|
||||
order = kwargs['order']
|
||||
event = sender
|
||||
|
||||
cls = list(event.checkin_lists.filter(auto_checkin_sales_channels=order.sales_channel).prefetch_related(
|
||||
'limit_products'))
|
||||
if not cls:
|
||||
return
|
||||
for op in order.positions.all():
|
||||
for cl in cls:
|
||||
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
|
||||
if not cl.subevent_id or cl.subevent_id == op.subevent_id:
|
||||
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True, type=Checkin.TYPE_ENTRY)
|
||||
checkin_created.send(event, checkin=ci)
|
||||
|
||||
|
||||
@receiver(periodic_task, dispatch_uid="autocheckout_exit_all")
|
||||
@scopes_disabled()
|
||||
def process_exit_all(sender, **kwargs):
|
||||
|
||||
@@ -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``.
|
||||
|
||||
@@ -33,9 +33,7 @@ from django_scopes.forms import (
|
||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||
from pretix.base.models import Gate
|
||||
from pretix.base.models.checkin import Checkin, CheckinList
|
||||
from pretix.control.forms import (
|
||||
ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
|
||||
)
|
||||
from pretix.control.forms import ItemMultipleChoiceField
|
||||
from pretix.control.forms.widgets import Select2
|
||||
|
||||
|
||||
@@ -67,10 +65,6 @@ class CheckinListForm(forms.ModelForm):
|
||||
kwargs.pop('locales', None)
|
||||
super().__init__(**kwargs)
|
||||
self.fields['limit_products'].queryset = self.event.items.all()
|
||||
self.fields['auto_checkin_sales_channels'].queryset = self.event.organizer.sales_channels.all()
|
||||
self.fields['auto_checkin_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(
|
||||
self.event, choices=self.fields['auto_checkin_sales_channels'].widget.choices
|
||||
)
|
||||
|
||||
if not self.event.organizer.gates.exists():
|
||||
del self.fields['gates']
|
||||
@@ -102,7 +96,6 @@ class CheckinListForm(forms.ModelForm):
|
||||
'limit_products',
|
||||
'subevent',
|
||||
'include_pending',
|
||||
'auto_checkin_sales_channels',
|
||||
'allow_multiple_entries',
|
||||
'allow_entry_after_exit',
|
||||
'rules',
|
||||
@@ -125,7 +118,6 @@ class CheckinListForm(forms.ModelForm):
|
||||
'limit_products': ItemMultipleChoiceField,
|
||||
'gates': SafeModelMultipleChoiceField,
|
||||
'subevent': SafeModelChoiceField,
|
||||
'auto_checkin_sales_channels': SafeModelMultipleChoiceField,
|
||||
'exit_all_at': NextTimeField,
|
||||
}
|
||||
|
||||
|
||||
@@ -1967,7 +1967,7 @@ class CheckinListAttendeeFilterForm(FilterForm):
|
||||
if s == '1':
|
||||
qs = qs.filter(last_entry__isnull=False)
|
||||
elif s == '2':
|
||||
qs = qs.filter(pk__in=self.list.positions_inside.values_list('pk'))
|
||||
qs = self.list._filter_positions_inside(qs)
|
||||
elif s == '3':
|
||||
qs = qs.filter(last_entry__isnull=False).filter(
|
||||
Q(last_exit__isnull=False) & Q(last_exit__gte=F('last_entry'))
|
||||
|
||||
@@ -609,6 +609,49 @@ class OrderFeeChangeForm(forms.Form):
|
||||
change_decimal_field(self.fields['value'], instance.order.event.currency)
|
||||
|
||||
|
||||
class OrderFeeAddForm(forms.Form):
|
||||
fee_type = forms.ChoiceField(choices=OrderFee.FEE_TYPES)
|
||||
value = forms.DecimalField(
|
||||
max_digits=13, decimal_places=2,
|
||||
localize=True,
|
||||
label=_('Price'),
|
||||
help_text=_("including all taxes"),
|
||||
)
|
||||
tax_rule = forms.ModelChoiceField(
|
||||
TaxRule.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
description = forms.CharField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
order = kwargs.pop('order')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['tax_rule'].queryset = order.event.tax_rules.all()
|
||||
change_decimal_field(self.fields['value'], order.event.currency)
|
||||
|
||||
|
||||
class OrderFeeAddFormset(forms.BaseFormSet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.order = kwargs.pop('order', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _construct_form(self, i, **kwargs):
|
||||
kwargs['order'] = self.order
|
||||
return super()._construct_form(i, **kwargs)
|
||||
|
||||
@property
|
||||
def empty_form(self):
|
||||
form = self.form(
|
||||
auto_id=self.auto_id,
|
||||
prefix=self.add_prefix('__prefix__'),
|
||||
empty_permitted=True,
|
||||
use_required_attribute=False,
|
||||
order=self.order,
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
|
||||
|
||||
class OrderContactForm(forms.ModelForm):
|
||||
regenerate_secrets = forms.BooleanField(required=False, label=_('Invalidate secrets'),
|
||||
help_text=_('Regenerates the order and ticket secrets. You will '
|
||||
|
||||
@@ -51,6 +51,7 @@ 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
|
||||
|
||||
@@ -639,6 +640,16 @@ 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'))
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||
{% bootstrap_field form.addon_match layout="control" %}
|
||||
{% bootstrap_field form.exit_all_at layout="control" %}
|
||||
{% bootstrap_field form.auto_checkin_sales_channels layout="control" %}
|
||||
{% if form.gates %}
|
||||
{% bootstrap_field form.gates layout="control" %}
|
||||
{% endif %}
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
<a href="?{% url_replace request 'ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
{% endif %}
|
||||
<th class="iconcol">{% trans "Automated check-in" %}</th>
|
||||
<th>{% trans "Products" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
@@ -137,17 +136,6 @@
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<td>
|
||||
{% for channel in cl.auto_checkin_sales_channels.all %}
|
||||
{% if "." in channel.icon %}
|
||||
<img src="{% static channel.icon %}" class="fa-like-image"
|
||||
data-toggle="tooltip" title="{{ channel.label }}">
|
||||
{% else %}
|
||||
<span class="fa fa-{{ channel.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{{ channel.label }}"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{% if cl.all_products %}
|
||||
<em>{% trans "All" %}</em>
|
||||
|
||||
@@ -12,10 +12,22 @@
|
||||
{% endif %}
|
||||
{% if object.id and not object.quotas.exists %}
|
||||
<div class="alert alert-warning">
|
||||
{% 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 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>
|
||||
</div>
|
||||
{% elif object.pk and not object.is_available_by_time %}
|
||||
<div class="alert alert-warning">
|
||||
|
||||
@@ -67,10 +67,22 @@
|
||||
<div class="panel-body form-horizontal">
|
||||
{% if form.instance.pk and not form.instance.quotas.exists %}
|
||||
<div class="alert alert-warning">
|
||||
{% 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 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>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_form_errors form %}
|
||||
|
||||
@@ -16,8 +16,18 @@
|
||||
{% 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-lg-9" %}
|
||||
{% bootstrap_field form.cross_selling_condition layout="control" horizontal_field_class="col-lg-9" %}
|
||||
{% bootstrap_field form.category_type layout="control" horizontal_field_class="big-radio-wrapper col-md-9" %}
|
||||
<div class="row" data-display-dependency="#id_category_type_2">
|
||||
<div class="col-md-offset-3 col-md-9">
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
Please note that cross-selling categories are intended as a marketing feature and are not
|
||||
suitable for strictly ensuring that products are only available in certain combinations.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.cross_selling_condition layout="control" horizontal_field_class="col-md-9" %}
|
||||
{% bootstrap_field form.cross_selling_match_products layout="control" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@@ -296,11 +296,11 @@
|
||||
{% endfor %}
|
||||
|
||||
|
||||
<div class="formset" data-formset data-formset-prefix="{{ add_formset.prefix }}">
|
||||
{{ add_formset.management_form }}
|
||||
{% bootstrap_formset_errors add_formset %}
|
||||
<div class="formset" data-formset data-formset-prefix="{{ add_position_formset.prefix }}">
|
||||
{{ add_position_formset.management_form }}
|
||||
{% bootstrap_formset_errors add_position_formset %}
|
||||
<div data-formset-body>
|
||||
{% for add_form in add_formset %}
|
||||
{% for add_form in add_position_formset %}
|
||||
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
@@ -351,25 +351,25 @@
|
||||
</button>
|
||||
{% trans "Add product" %}
|
||||
<div class="sr-only">
|
||||
{{ add_formset.empty_form.id }}
|
||||
{% bootstrap_field add_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
{{ add_position_formset.empty_form.id }}
|
||||
{% bootstrap_field add_position_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_field add_formset.empty_form.itemvar layout="control" %}
|
||||
{% bootstrap_field add_formset.empty_form.price addon_after=request.event.currency layout="control" %}
|
||||
{% if add_formset.empty_form.addon_to %}
|
||||
{% bootstrap_field add_formset.empty_form.addon_to layout="control" %}
|
||||
{% bootstrap_field add_position_formset.empty_form.itemvar layout="control" %}
|
||||
{% bootstrap_field add_position_formset.empty_form.price addon_after=request.event.currency layout="control" %}
|
||||
{% if add_position_formset.empty_form.addon_to %}
|
||||
{% bootstrap_field add_position_formset.empty_form.addon_to layout="control" %}
|
||||
{% endif %}
|
||||
{% if add_formset.empty_form.subevent %}
|
||||
{% bootstrap_field add_formset.empty_form.subevent layout="control" %}
|
||||
{% if add_position_formset.empty_form.subevent %}
|
||||
{% bootstrap_field add_position_formset.empty_form.subevent layout="control" %}
|
||||
{% endif %}
|
||||
{% if add_formset.empty_form.used_membership %}
|
||||
{% bootstrap_field add_formset.empty_form.used_membership layout="control" %}
|
||||
{% if add_position_formset.empty_form.used_membership %}
|
||||
{% bootstrap_field add_position_formset.empty_form.used_membership layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field add_formset.empty_form.seat layout="control" %}
|
||||
{% bootstrap_field add_position_formset.empty_form.seat layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -431,13 +431,77 @@
|
||||
{% bootstrap_field fee.form.operation_cancel layout='inline' %}
|
||||
{% if fee.fee_type == "payment" %}
|
||||
<em class="text-danger">
|
||||
{% trans "Manually modifying payment fees is discouraged since they might automatically be on subsequent order changes or when choosing a different payment method." %}
|
||||
{% trans "Manually modifying payment fees is discouraged since they might automatically be updated on subsequent order changes or when choosing a different payment method." %}
|
||||
</em>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="formset" data-formset data-formset-prefix="{{ add_fee_formset.prefix }}">
|
||||
{{ add_fee_formset.management_form }}
|
||||
{% bootstrap_formset_errors add_fee_formset %}
|
||||
<div data-formset-body>
|
||||
{% for add_form in add_fee_formset %}
|
||||
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<button type="button" class="btn btn-danger btn-xs pull-right flip"
|
||||
data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
{% trans "Add fee" %}
|
||||
<div class="sr-only">
|
||||
{{ add_form.id }}
|
||||
{% bootstrap_field add_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_field add_form.fee_type layout='control' %}
|
||||
{% bootstrap_field add_form.value addon_after=request.event.currency layout='control' %}
|
||||
{% bootstrap_field add_form.tax_rule layout='control' %}
|
||||
{% bootstrap_field add_form.description layout='control' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<button type="button" class="btn btn-danger btn-xs pull-right flip"
|
||||
data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
{% trans "Add fee" %}
|
||||
<div class="sr-only">
|
||||
{{ add_fee_formset.empty_form.id }}
|
||||
{% bootstrap_field add_fee_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_field add_fee_formset.empty_form.fee_type layout='control' %}
|
||||
{% bootstrap_field add_fee_formset.empty_form.value addon_after=request.event.currency layout='control' %}
|
||||
{% bootstrap_field add_fee_formset.empty_form.tax_rule layout='control' %}
|
||||
{% bootstrap_field add_fee_formset.empty_form.description layout='control' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-primary" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add fee" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% if order.status == "n" %}
|
||||
{% if order.status == "n" and not order.require_approval %}
|
||||
<dt>{% trans "Expiry date" %}</dt>
|
||||
<dd>
|
||||
{{ order.expires|date:"SHORT_DATETIME_FORMAT" }}
|
||||
|
||||
@@ -299,7 +299,7 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related(
|
||||
"limit_products", "auto_checkin_sales_channels"
|
||||
"limit_products",
|
||||
)
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
|
||||
@@ -257,7 +257,7 @@ class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
if form.has_changed():
|
||||
self.object.log_action(
|
||||
'pretix.event.category.reordered', user=self.request.user, data={
|
||||
'pretix.event.category.changed', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
}
|
||||
)
|
||||
@@ -920,16 +920,19 @@ 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):
|
||||
|
||||
@@ -122,10 +122,11 @@ from pretix.control.forms.filter import (
|
||||
)
|
||||
from pretix.control.forms.orders import (
|
||||
CancelForm, CommentForm, DenyForm, EventCancelForm, ExporterForm,
|
||||
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
|
||||
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
||||
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
|
||||
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeAddForm,
|
||||
OrderFeeAddFormset, OrderFeeChangeForm, OrderLocaleForm, OrderMailForm,
|
||||
OrderPositionAddForm, OrderPositionAddFormset, OrderPositionChangeForm,
|
||||
OrderPositionMailForm, OrderRefundForm, OtherOperationsForm,
|
||||
ReactivateOrderForm,
|
||||
)
|
||||
from pretix.control.forms.rrule import RRuleForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
@@ -1874,18 +1875,30 @@ class OrderChange(OrderView):
|
||||
data=self.request.POST if self.request.method == "POST" else None)
|
||||
|
||||
@cached_property
|
||||
def add_formset(self):
|
||||
def add_position_formset(self):
|
||||
ff = formset_factory(
|
||||
OrderPositionAddForm, formset=OrderPositionAddFormset,
|
||||
can_order=False, can_delete=True, extra=0
|
||||
)
|
||||
return ff(
|
||||
prefix='add',
|
||||
prefix='add_position',
|
||||
order=self.order,
|
||||
items=self.items,
|
||||
data=self.request.POST if self.request.method == "POST" else None
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def add_fee_formset(self):
|
||||
ff = formset_factory(
|
||||
OrderFeeAddForm, formset=OrderFeeAddFormset,
|
||||
can_order=False, can_delete=True, extra=0
|
||||
)
|
||||
return ff(
|
||||
prefix='add_fee',
|
||||
order=self.order,
|
||||
data=self.request.POST if self.request.method == "POST" else None
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def items(self):
|
||||
return self.request.event.items.prefetch_related('variations', 'tax_rule').all()
|
||||
@@ -1914,7 +1927,8 @@ class OrderChange(OrderView):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['positions'] = self.positions
|
||||
ctx['fees'] = self.fees
|
||||
ctx['add_formset'] = self.add_formset
|
||||
ctx['add_position_formset'] = self.add_position_formset
|
||||
ctx['add_fee_formset'] = self.add_fee_formset
|
||||
ctx['other_form'] = self.other_form
|
||||
ctx['use_revocation_list'] = self.request.event.ticket_secret_generator.use_revocation_list
|
||||
return ctx
|
||||
@@ -1929,12 +1943,35 @@ class OrderChange(OrderView):
|
||||
)
|
||||
return True
|
||||
|
||||
def _process_add(self, ocm):
|
||||
if not self.add_formset.is_valid():
|
||||
def _process_add_fees(self, ocm):
|
||||
if not self.add_fee_formset.is_valid():
|
||||
return False
|
||||
else:
|
||||
for f in self.add_formset.forms:
|
||||
if f in self.add_formset.deleted_forms or not f.has_changed():
|
||||
for f in self.add_fee_formset.forms:
|
||||
if f in self.add_fee_formset.deleted_forms or not f.has_changed():
|
||||
continue
|
||||
|
||||
f = OrderFee(
|
||||
fee_type=f.cleaned_data['fee_type'],
|
||||
value=f.cleaned_data['value'],
|
||||
order=ocm.order,
|
||||
tax_rule=f.cleaned_data['tax_rule'],
|
||||
description=f.cleaned_data['description'],
|
||||
)
|
||||
f._calculate_tax()
|
||||
try:
|
||||
ocm.add_fee(f)
|
||||
except OrderError as e:
|
||||
f.custom_error = str(e)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _process_add_positions(self, ocm):
|
||||
if not self.add_position_formset.is_valid():
|
||||
return False
|
||||
else:
|
||||
for f in self.add_position_formset.forms:
|
||||
if f in self.add_position_formset.deleted_forms or not f.has_changed():
|
||||
continue
|
||||
|
||||
if '-' in f.cleaned_data['itemvar']:
|
||||
@@ -1959,7 +1996,7 @@ class OrderChange(OrderView):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _process_fees(self, ocm):
|
||||
def _process_change_fees(self, ocm):
|
||||
for f in self.fees:
|
||||
if not f.form.is_valid():
|
||||
return False
|
||||
@@ -1980,7 +2017,7 @@ class OrderChange(OrderView):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _process_change(self, ocm):
|
||||
def _process_change_positions(self, ocm):
|
||||
for p in self.positions:
|
||||
if not p.form.is_valid():
|
||||
return False
|
||||
@@ -2061,7 +2098,11 @@ class OrderChange(OrderView):
|
||||
notify=notify,
|
||||
reissue_invoice=self.other_form.cleaned_data['reissue_invoice'] if self.other_form.is_valid() else True
|
||||
)
|
||||
form_valid = self._process_add(ocm) and self._process_fees(ocm) and self._process_change(ocm) and self._process_other(ocm)
|
||||
form_valid = (self._process_add_fees(ocm) and
|
||||
self._process_add_positions(ocm) and
|
||||
self._process_change_fees(ocm) and
|
||||
self._process_change_positions(ocm) and
|
||||
self._process_other(ocm))
|
||||
|
||||
if not form_valid:
|
||||
messages.error(self.request, _('An error occurred. Please see the details below.'))
|
||||
|
||||
@@ -156,7 +156,7 @@ def event_list(request):
|
||||
max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from'))
|
||||
).annotate(
|
||||
order_from=Coalesce('min_from', 'date_from'),
|
||||
).order_by('-order_from')
|
||||
).order_by('-order_from', 'slug')
|
||||
|
||||
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')
|
||||
).order_by('-order_from', 'slug')
|
||||
|
||||
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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+648
-582
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:14+0000\n"
|
||||
"POT-Creation-Date: 2024-10-29 08:54+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"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1648
-1750
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ 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"
|
||||
"PO-Revision-Date: 2024-11-01 19: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"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"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.8.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -326,13 +326,13 @@ msgstr "Tickets válidos"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
||||
msgid "Currently inside"
|
||||
msgstr "Actualmente adentro"
|
||||
msgstr "Actualmente en el interior"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||
msgid "Yes"
|
||||
msgstr "Sí"
|
||||
msgstr "Si"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
|
||||
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
||||
@@ -570,10 +570,8 @@ msgid "Group of objects"
|
||||
msgstr "Grupo de objetos"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:899
|
||||
#, fuzzy
|
||||
#| msgid "Text object"
|
||||
msgid "Text object (deprecated)"
|
||||
msgstr "Objeto de texto"
|
||||
msgstr "Objeto texto (obsoleto)"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:901
|
||||
msgid "Text box"
|
||||
@@ -704,12 +702,12 @@ msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they’re available."
|
||||
msgstr ""
|
||||
"Los elementos en su carrito de compras ya no se encuentran reservados. "
|
||||
"Puedes seguir añadiendo más productos mientras estén disponibles."
|
||||
"Los elementos en su cesta de compras ya no se encuentran reservados. Puedes "
|
||||
"seguir añadiendo más productos mientras estén disponibles."
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:45
|
||||
msgid "Cart expired"
|
||||
msgstr "El carrito de compras ha expirado"
|
||||
msgstr "La cesta de compra ha expirado"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:50
|
||||
msgid "The items in your cart are reserved for you for one minute."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgstr ""
|
||||
"Project-Id-Version: French\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 11:06+0000\n"
|
||||
"PO-Revision-Date: 2024-06-12 03:00+0000\n"
|
||||
"Last-Translator: simonD <simon.dalvy@gmail.com>\n"
|
||||
"PO-Revision-Date: 2024-11-01 02:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"fr/>\n"
|
||||
"Language: fr\n"
|
||||
@@ -16,7 +16,7 @@ msgstr ""
|
||||
"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.5.5\n"
|
||||
"X-Generator: Weblate 5.8.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -293,7 +293,7 @@ msgstr "Entrée non autorisée"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||
msgid "Ticket code revoked/changed"
|
||||
msgstr "Code du billet révoqué/modifié"
|
||||
msgstr "Code ticket révoqué/modifié"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||
msgid "Ticket blocked"
|
||||
@@ -452,7 +452,6 @@ msgid "Product variation"
|
||||
msgstr "Variation du produit"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
|
||||
#, fuzzy
|
||||
msgid "Gate"
|
||||
msgstr "Pont"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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