mirror of
https://github.com/pretix/pretix.git
synced 2025-12-14 13:32:28 +00:00
Compare commits
1 Commits
v2025.9.0
...
fix-widget
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2cbf82700 |
@@ -1,7 +1,7 @@
|
|||||||
This file is part of pretix (Community Edition).
|
This file is part of pretix (Community Edition).
|
||||||
|
|
||||||
Copyright (C) 2014-2020 Raphael Michel and contributors
|
Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
Copyright (C) 2020-today pretix GmbH and contributors
|
Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
Public License as published by the Free Software Foundation in version 3 of the License.
|
Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ pretix
|
|||||||
:target: https://docs.pretix.eu/
|
:target: https://docs.pretix.eu/
|
||||||
|
|
||||||
.. image:: https://github.com/pretix/pretix/workflows/Tests/badge.svg
|
.. image:: https://github.com/pretix/pretix/workflows/Tests/badge.svg
|
||||||
:target: https://github.com/pretix/pretix/actions/workflows/tests.yml
|
|
||||||
|
|
||||||
.. image:: https://codecov.io/gh/pretix/pretix/branch/master/graph/badge.svg
|
.. image:: https://codecov.io/gh/pretix/pretix/branch/master/graph/badge.svg
|
||||||
:target: https://codecov.io/gh/pretix/pretix
|
:target: https://codecov.io/gh/pretix/pretix
|
||||||
|
|||||||
@@ -359,65 +359,3 @@ Performing a ticket search
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer or check-in list does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer or check-in list does not exist **or** you have no permission to view this resource.
|
||||||
:statuscode 404: The requested check-in list does not exist.
|
:statuscode 404: The requested check-in list does not exist.
|
||||||
|
|
||||||
.. _`rest-checkin-annul`:
|
|
||||||
|
|
||||||
Annulment of a check-in
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/checkinrpc/annul/
|
|
||||||
|
|
||||||
If a check-in was made in error and the person was not let in, it can be annulled. We do not recommend this to be used
|
|
||||||
in case of manual check-ins or user interfaces because it is too prone for human errors. It is mostly intended for
|
|
||||||
automated entry systems like a turnstile or automated door, where the check-in is first created, then the door is
|
|
||||||
opened, and then the check-in may be annulled if the system knows that the turnstile did not turn or was out of
|
|
||||||
order.
|
|
||||||
|
|
||||||
This endpoint supports passing multiple check-in lists for the context of a multi-event scan. However, each
|
|
||||||
check-in list passed needs to be from a distinct event.
|
|
||||||
|
|
||||||
Check-ins created by a device can only be annulled by the same device. The datetime of annulment may not be more than
|
|
||||||
15 minutes after the datetime of check-in (value subject to change).
|
|
||||||
|
|
||||||
A status code of 404 is returned if no check-in was found for the given nonce. A status code of 400 is returned when
|
|
||||||
multiple check-ins match the nonce, the input is invalid in another way, the annulment is made from the wrong device,
|
|
||||||
the check-in is already in an annulled or failed state, or the datetime constraint is not valid.
|
|
||||||
|
|
||||||
:<json string nonce: ``nonce`` value of the original check-in.
|
|
||||||
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
|
|
||||||
:<json datetime datetime: Specifies the client-side datetime of the annulment. If not supplied, the current time will be used.
|
|
||||||
:<json string error_explanation: A human-readable description of why the check-in was annulled (optional).
|
|
||||||
:>json string status: ``"ok"``
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /api/v1/organizers/bigevents/checkinrpc/annul/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
{
|
|
||||||
"lists": [1],
|
|
||||||
"nonce": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
|
|
||||||
"error_explanation": "Turnstile did not turn"
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example successful response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 400: Invalid or incomplete request, see above
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
:statuscode 404: The requested nonce does not exist.
|
|
||||||
|
|||||||
@@ -424,9 +424,9 @@ Endpoints
|
|||||||
:param organizer: The ``slug`` field of the organizer of the event to create.
|
:param organizer: The ``slug`` field of the organizer of the event to create.
|
||||||
:param event: The ``slug`` field of the event to copy settings and items from.
|
:param event: The ``slug`` field of the event to copy settings and items from.
|
||||||
:statuscode 201: no error
|
:statuscode 201: no error
|
||||||
:statuscode 400: The event could not be updated due to invalid submitted data.
|
:statuscode 400: The event could not be created due to invalid submitted data.
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to update this resource.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||||
|
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/
|
||||||
|
|||||||
@@ -349,45 +349,6 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/vouchers/bulk_attach/
|
|
||||||
|
|
||||||
Attaches many **existing** vouchers to an exhibitor. You need to send either the ``id`` **or** the ``code`` field of
|
|
||||||
the voucher, but you need to send the same field for all entries.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/vouchers/bulk_attach/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 15,
|
|
||||||
"exhibitor_comment": "Free ticket"
|
|
||||||
},
|
|
||||||
..
|
|
||||||
]
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
|
||||||
:param event: The ``slug`` field of the event to use
|
|
||||||
:param id: The ``id`` field of the exhibitor to use
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 400: Invalid data sent, e.g. voucher does not exist
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/
|
||||||
|
|
||||||
Create a new exhibitor.
|
Create a new exhibitor.
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ at :ref:`plugin-docs`.
|
|||||||
seats
|
seats
|
||||||
orders
|
orders
|
||||||
invoices
|
invoices
|
||||||
transactions
|
|
||||||
vouchers
|
vouchers
|
||||||
discounts
|
discounts
|
||||||
checkin
|
checkin
|
||||||
@@ -55,7 +54,6 @@ at :ref:`plugin-docs`.
|
|||||||
digital
|
digital
|
||||||
exhibitors
|
exhibitors
|
||||||
imported_secrets
|
imported_secrets
|
||||||
offlinesales
|
|
||||||
shipping
|
shipping
|
||||||
billing_invoices
|
billing_invoices
|
||||||
billing_var
|
billing_var
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
.. _rest-invoices:
|
|
||||||
|
|
||||||
Invoices
|
Invoices
|
||||||
========
|
========
|
||||||
|
|
||||||
@@ -26,8 +24,6 @@ invoice_from_country string Sender address:
|
|||||||
invoice_from_tax_id string Sender address: Local Tax ID
|
invoice_from_tax_id string Sender address: Local Tax ID
|
||||||
invoice_from_vat_id string Sender address: EU VAT ID
|
invoice_from_vat_id string Sender address: EU VAT ID
|
||||||
invoice_to string Full recipient address
|
invoice_to string Full recipient address
|
||||||
invoice_to_is_business boolean Recipient address: Business vs individual (``null`` for
|
|
||||||
invoices created before pretix 2025.6).
|
|
||||||
invoice_to_company string Recipient address: Company name
|
invoice_to_company string Recipient address: Company name
|
||||||
invoice_to_name string Recipient address: Person name
|
invoice_to_name string Recipient address: Person name
|
||||||
invoice_to_street string Recipient address: Address lines
|
invoice_to_street string Recipient address: Address lines
|
||||||
@@ -37,7 +33,6 @@ invoice_to_state string Recipient addre
|
|||||||
invoice_to_country string Recipient address: Country code
|
invoice_to_country string Recipient address: Country code
|
||||||
invoice_to_vat_id string Recipient address: EU VAT ID
|
invoice_to_vat_id string Recipient address: EU VAT ID
|
||||||
invoice_to_beneficiary string Invoice beneficiary
|
invoice_to_beneficiary string Invoice beneficiary
|
||||||
invoice_to_transmission_info object Additional transmission info (see :ref:`rest-transmission-types`)
|
|
||||||
custom_field string Custom invoice address field
|
custom_field string Custom invoice address field
|
||||||
date date Invoice date
|
date date Invoice date
|
||||||
refers string Invoice number of an invoice this invoice refers to
|
refers string Invoice number of an invoice this invoice refers to
|
||||||
@@ -80,12 +75,17 @@ lines list of objects The actual invo
|
|||||||
for all invoice lines
|
for all invoice lines
|
||||||
created before this field was introduced as well as for
|
created before this field was introduced as well as for
|
||||||
all lines not created by a fee (e.g. a product).
|
all lines not created by a fee (e.g. a product).
|
||||||
├ period_start datetime Start date of the service or delivery period of the invoice line.
|
├ event_date_from datetime Start date of the (sub)event this line was created for as it
|
||||||
Can be ``null`` if not known.
|
was set during invoice creation. Can be ``null`` for all invoice
|
||||||
├ period_end datetime End date of the service or delivery period of the invoice line.
|
lines created before this was introduced as well as for lines in
|
||||||
Can be ``null`` if not known.
|
an event series not created by a product (e.g. shipping or
|
||||||
├ event_date_from datetime Deprecated alias of ``period_start``.
|
cancellation fees).
|
||||||
├ event_date_to datetime Deprecated alias of ``period_end``.
|
├ event_date_to datetime End date of the (sub)event this line was created for as it
|
||||||
|
was set during invoice creation. Can be ``null`` for all invoice
|
||||||
|
lines created before this was introduced as well as for lines in
|
||||||
|
an event series not created by a product (e.g. shipping or
|
||||||
|
cancellation fees) as well as whenever the respective (sub)event
|
||||||
|
has no end date set.
|
||||||
├ event_location string Location of the (sub)event this line was created for as it
|
├ event_location string Location of the (sub)event this line was created for as it
|
||||||
was set during invoice creation. Can be ``null`` for all invoice
|
was set during invoice creation. Can be ``null`` for all invoice
|
||||||
lines created before this was introduced as well as for lines in
|
lines created before this was introduced as well as for lines in
|
||||||
@@ -108,12 +108,6 @@ foreign_currency_rate decimal (string) If ``foreign_cu
|
|||||||
foreign_currency_rate_date date If ``foreign_currency_rate`` is set, this signifies the
|
foreign_currency_rate_date date If ``foreign_currency_rate`` is set, this signifies the
|
||||||
date at which the currency rate was obtained.
|
date at which the currency rate was obtained.
|
||||||
internal_reference string Customer's reference to be printed on the invoice.
|
internal_reference string Customer's reference to be printed on the invoice.
|
||||||
transmission_type string Requested transmission channel (see :ref:`rest-transmission-types`)
|
|
||||||
transmission_provider string Selected transmission provider (depends on installed
|
|
||||||
plugins). ``null`` if not yet chosen.
|
|
||||||
transmission_status string Transmission status, one of ``unknown`` (pre-2025.6),
|
|
||||||
``pending``, ``inflight``, ``failed``, and ``completed``.
|
|
||||||
transmission_date datetime Time of last change in transmission status (may be ``null``).
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -125,76 +119,6 @@ transmission_date datetime Time of last ch
|
|||||||
|
|
||||||
The ``tax_code`` attribute has been added.
|
The ``tax_code`` attribute has been added.
|
||||||
|
|
||||||
.. versionchanged:: 2025.6
|
|
||||||
|
|
||||||
The attributes ``invoice_to_is_business``, ``invoice_to_transmission_info``, ``transmission_type``,
|
|
||||||
``transmission_provider``, ``transmission_status``, and ``transmission_date`` have been added.
|
|
||||||
|
|
||||||
|
|
||||||
.. _`rest-transmission-types`:
|
|
||||||
|
|
||||||
Transmission types
|
|
||||||
------------------
|
|
||||||
|
|
||||||
pretix supports multiple ways to transmit an invoice from the organizer to the invoice recipient.
|
|
||||||
For each transmission type, different fields are supported in the ``transmission_info`` object of the
|
|
||||||
invoice address. Currently, pretix supports the following transmission types:
|
|
||||||
|
|
||||||
Email
|
|
||||||
"""""
|
|
||||||
|
|
||||||
The identifier ``"email"`` represents the transmission of PDF invoices through email.
|
|
||||||
This is the default transmission type in pretix and has some special behavior for backwards compatibility.
|
|
||||||
Transmission is always executed through the provider ``"email_pdf"``.
|
|
||||||
The ``transmission_info`` object may contain the following properties:
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
transmission_email_address string Optional. An email address other than the order address
|
|
||||||
that the invoice should be sent to.
|
|
||||||
Business customers only.
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
Peppol
|
|
||||||
""""""
|
|
||||||
|
|
||||||
The identifier ``"peppol"`` represents the transmission of XML invoices through the `Peppol`_ network.
|
|
||||||
This is only available for business addresses.
|
|
||||||
This is not supported by pretix out of the box and requires the use of a suitable plugin.
|
|
||||||
The ``transmission_info`` object may contain the following properties:
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
transmission_peppol_participant_id string Required. The Peppol participant ID of the recipient.
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
Italian Exchange System
|
|
||||||
"""""""""""""""""""""""
|
|
||||||
|
|
||||||
The identifier ``"it_sdi"`` represents the transmission of XML invoices through the `Sistema di Interscambio`_ network used in Italy.
|
|
||||||
This is only available for addresses with country ``"IT"``.
|
|
||||||
This is not supported by pretix out of the box and requires the use of a suitable plugin.
|
|
||||||
The ``transmission_info`` object may contain the following properties:
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
transmission_it_sdi_codice_fiscale string Required for non-business address. Fiscal code of the
|
|
||||||
recipient.
|
|
||||||
transmission_it_sdi_pec string Required for business addresses. Address for certified
|
|
||||||
electronic mail.
|
|
||||||
transmission_it_sdi_recipient_code string Required for businesses. SdI recipient code.
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
If this type is selected, ``vat_id`` is required for business addresses.
|
|
||||||
|
|
||||||
List of all invoices
|
List of all invoices
|
||||||
--------------------
|
--------------------
|
||||||
@@ -238,7 +162,6 @@ List of all invoices
|
|||||||
"invoice_from_vat_id":"",
|
"invoice_from_vat_id":"",
|
||||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
||||||
"invoice_to_company": "Sample company",
|
"invoice_to_company": "Sample company",
|
||||||
"invoice_to_is_business": true,
|
|
||||||
"invoice_to_name": "John Doe",
|
"invoice_to_name": "John Doe",
|
||||||
"invoice_to_street": "Test street 12",
|
"invoice_to_street": "Test street 12",
|
||||||
"invoice_to_zipcode": "12345",
|
"invoice_to_zipcode": "12345",
|
||||||
@@ -247,7 +170,6 @@ List of all invoices
|
|||||||
"invoice_to_country": "TE",
|
"invoice_to_country": "TE",
|
||||||
"invoice_to_vat_id": "EU123456789",
|
"invoice_to_vat_id": "EU123456789",
|
||||||
"invoice_to_beneficiary": "",
|
"invoice_to_beneficiary": "",
|
||||||
"invoice_to_transmission_info": {},
|
|
||||||
"custom_field": null,
|
"custom_field": null,
|
||||||
"date": "2017-12-01",
|
"date": "2017-12-01",
|
||||||
"refers": null,
|
"refers": null,
|
||||||
@@ -269,8 +191,6 @@ List of all invoices
|
|||||||
"fee_internal_type": null,
|
"fee_internal_type": null,
|
||||||
"event_date_from": "2017-12-27T10:00:00Z",
|
"event_date_from": "2017-12-27T10:00:00Z",
|
||||||
"event_date_to": null,
|
"event_date_to": null,
|
||||||
"period_start": "2017-12-27T10:00:00Z",
|
|
||||||
"period_end": "2017-12-27T10:00:00Z",
|
|
||||||
"event_location": "Heidelberg",
|
"event_location": "Heidelberg",
|
||||||
"attendee_name": null,
|
"attendee_name": null,
|
||||||
"gross_value": "23.00",
|
"gross_value": "23.00",
|
||||||
@@ -282,11 +202,7 @@ List of all invoices
|
|||||||
],
|
],
|
||||||
"foreign_currency_display": "PLN",
|
"foreign_currency_display": "PLN",
|
||||||
"foreign_currency_rate": "4.2408",
|
"foreign_currency_rate": "4.2408",
|
||||||
"foreign_currency_rate_date": "2017-07-24",
|
"foreign_currency_rate_date": "2017-07-24"
|
||||||
"transmission_type": "email",
|
|
||||||
"transmission_provider": "email_pdf",
|
|
||||||
"transmission_status": "completed",
|
|
||||||
"transmission_date": "2017-07-24T10:00:00Z"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -386,7 +302,6 @@ Fetching individual invoices
|
|||||||
"invoice_from_vat_id":"",
|
"invoice_from_vat_id":"",
|
||||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
||||||
"invoice_to_company": "Sample company",
|
"invoice_to_company": "Sample company",
|
||||||
"invoice_to_is_business": true,
|
|
||||||
"invoice_to_name": "John Doe",
|
"invoice_to_name": "John Doe",
|
||||||
"invoice_to_street": "Test street 12",
|
"invoice_to_street": "Test street 12",
|
||||||
"invoice_to_zipcode": "12345",
|
"invoice_to_zipcode": "12345",
|
||||||
@@ -395,7 +310,6 @@ Fetching individual invoices
|
|||||||
"invoice_to_country": "TE",
|
"invoice_to_country": "TE",
|
||||||
"invoice_to_vat_id": "EU123456789",
|
"invoice_to_vat_id": "EU123456789",
|
||||||
"invoice_to_beneficiary": "",
|
"invoice_to_beneficiary": "",
|
||||||
"invoice_to_transmission_info": {},
|
|
||||||
"custom_field": null,
|
"custom_field": null,
|
||||||
"date": "2017-12-01",
|
"date": "2017-12-01",
|
||||||
"refers": null,
|
"refers": null,
|
||||||
@@ -417,8 +331,6 @@ Fetching individual invoices
|
|||||||
"fee_internal_type": null,
|
"fee_internal_type": null,
|
||||||
"event_date_from": "2017-12-27T10:00:00Z",
|
"event_date_from": "2017-12-27T10:00:00Z",
|
||||||
"event_date_to": null,
|
"event_date_to": null,
|
||||||
"period_start": "2017-12-27T10:00:00Z",
|
|
||||||
"period_end": "2017-12-27T10:00:00Z",
|
|
||||||
"event_location": "Heidelberg",
|
"event_location": "Heidelberg",
|
||||||
"attendee_name": null,
|
"attendee_name": null,
|
||||||
"gross_value": "23.00",
|
"gross_value": "23.00",
|
||||||
@@ -430,11 +342,7 @@ Fetching individual invoices
|
|||||||
],
|
],
|
||||||
"foreign_currency_display": "PLN",
|
"foreign_currency_display": "PLN",
|
||||||
"foreign_currency_rate": "4.2408",
|
"foreign_currency_rate": "4.2408",
|
||||||
"foreign_currency_rate_date": "2017-07-24",
|
"foreign_currency_rate_date": "2017-07-24"
|
||||||
"transmission_type": "email",
|
|
||||||
"transmission_provider": "email_pdf",
|
|
||||||
"transmission_status": "completed",
|
|
||||||
"transmission_date": "2017-07-24T10:00:00Z"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -539,70 +447,3 @@ Invoices cannot be edited directly, but the following actions can be triggered:
|
|||||||
:statuscode 400: The invoice has already been canceled
|
:statuscode 400: The invoice has already been canceled
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
|
|
||||||
Transmitting invoices
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
Invoices are transmitted automatically when created during order creation or payment receipt,
|
|
||||||
but in other cases transmission may need to be triggered manually.
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/transmit/
|
|
||||||
|
|
||||||
Transmits the invoice to the recipient, but only if it is in ``pending`` state.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/transmit/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 204 No Content
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/pdf
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:param number: The ``number`` field of the invoice to transmit
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to transmit this invoice **or** the invoice may not be transmitted
|
|
||||||
:statuscode 409: The invoice is currently in transmission
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/retransmit/
|
|
||||||
|
|
||||||
Transmits the invoice to the recipient even if transmission was already attempted previously.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/retransmit/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 204 No Content
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/pdf
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:param number: The ``number`` field of the invoice to transmit
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to transmit this invoice **or** the invoice may not be transmitted
|
|
||||||
:statuscode 409: The invoice is currently in transmission
|
|
||||||
|
|
||||||
|
|
||||||
.. _Peppol: https://en.wikipedia.org/wiki/PEPPOL
|
|
||||||
.. _Sistema di Interscambio: https://it.wikipedia.org/wiki/Fattura_elettronica_in_Italia
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
Offline sales
|
|
||||||
=============
|
|
||||||
|
|
||||||
.. note:: This API is only available when the plugin **pretix-offlinesales** is installed (pretix Hosted and Enterprise only).
|
|
||||||
|
|
||||||
The offline sales module allows you to create batches of tickets intended for the sale outside the system.
|
|
||||||
|
|
||||||
Resource description
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
The offline sales batch resource contains the following public fields:
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
id integer Internal batch ID
|
|
||||||
creation datetime Time of creation
|
|
||||||
testmode boolean ``true`` if orders are created in test mode
|
|
||||||
sales_channel string Sales channel of the orders
|
|
||||||
layout integer Internal ID of the chosen ticket layout
|
|
||||||
subevent integer Internal ID of the chosen subevent (or ``null``)
|
|
||||||
item integer Internal ID of the chosen product
|
|
||||||
variation integer Internal ID of the chosen variation (or ``null``)
|
|
||||||
amount integer Number of tickets in the batch
|
|
||||||
comment string Internal comment
|
|
||||||
orders list of strings List of order codes (omitted in list view for performance reasons)
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
Endpoints
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/
|
|
||||||
|
|
||||||
Returns a list of all offline sales batches
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/democon/offlinesalesbatches/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: text/javascript
|
|
||||||
|
|
||||||
{
|
|
||||||
"count": 1,
|
|
||||||
"next": null,
|
|
||||||
"previous": null,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"creation": "2025-07-08T18:27:32.134368+02:00",
|
|
||||||
"testmode": False,
|
|
||||||
"sales_channel": "web",
|
|
||||||
"comment": "Batch for sale at the event",
|
|
||||||
"layout": 3,
|
|
||||||
"subevent": null,
|
|
||||||
"item": 23,
|
|
||||||
"variation": null,
|
|
||||||
"amount": 7
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:query page: The page number in case of a multi-page result set, default is 1
|
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
|
||||||
:param event: The ``slug`` field of a valid event
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/(id)/
|
|
||||||
|
|
||||||
Returns information on a given batch.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/democon/offlinesalesbatches/1/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: text/javascript
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"creation": "2025-07-08T18:27:32.134368+02:00",
|
|
||||||
"testmode": False,
|
|
||||||
"sales_channel": "web",
|
|
||||||
"comment": "Batch for sale at the event",
|
|
||||||
"layout": 3,
|
|
||||||
"subevent": null,
|
|
||||||
"item": 23,
|
|
||||||
"variation": null,
|
|
||||||
"amount": 7,
|
|
||||||
"orders": ["TSRNN", "3FBSL", "WMDNJ", "BHW9H", "MXSUG", "DSDAP", "URLLE"]
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:param id: The ``id`` field of the batch to fetch
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
|
||||||
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/
|
|
||||||
|
|
||||||
With this API call, you can instruct the system to create a new batch.
|
|
||||||
|
|
||||||
Since batches can contain up to 10,000 tickets, they are created asynchronously on the server.
|
|
||||||
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
|
||||||
The body points you to the check URL of the result. Running a ``GET`` request on that result URL will
|
|
||||||
yield one of the following status codes:
|
|
||||||
|
|
||||||
* ``200 OK`` – The creation of the batch has succeeded. The body will be your resulting batch with the same information as in the detail endpoint above.
|
|
||||||
* ``409 Conflict`` – Your creation job is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
|
||||||
* ``410 Gone`` – Creating the batch has failed permanently (e.g. quota no longer available). The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
|
||||||
* ``404 Not Found`` – The job does not exist / is expired.
|
|
||||||
|
|
||||||
.. note:: To avoid performance issues, a maximum amount of 10000 is currently allowed.
|
|
||||||
|
|
||||||
.. note:: Do not wait multiple hours or more to retrieve your result. After a longer wait time, ``409`` might be returned permanently due to technical constraints, even though nothing will happen any more.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"testmode": True,
|
|
||||||
"layout": 123,
|
|
||||||
"item": 14,
|
|
||||||
"sales_channel": "web",
|
|
||||||
"amount": 10,
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"check": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/check/29891ede-196f-4942-9e26-d055a36e98b8/"
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:statuscode 202: no error
|
|
||||||
:statuscode 400: Invalid input options
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/(id)/render/
|
|
||||||
|
|
||||||
With this API call, you can render the PDF representation of a batch.
|
|
||||||
|
|
||||||
Since batches can contain up to 10,000 tickets, they are rendered asynchronously on the server.
|
|
||||||
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
|
||||||
The body points you to the download URL of the result. Running a ``GET`` request on that result URL will
|
|
||||||
yield one of the following status codes:
|
|
||||||
|
|
||||||
* ``200 OK`` – The creation of the batch has succeeded. The body will be your resulting batch with the same information as in the detail endpoint above.
|
|
||||||
* ``409 Conflict`` – Your rendering process is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
|
||||||
* ``410 Gone`` – Rendering the batch has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
|
||||||
* ``404 Not Found`` – The rendering job does not exist / is expired.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/1/render HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/1/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:param id: The ``id`` field of the batch to fetch
|
|
||||||
:statuscode 202: no error
|
|
||||||
:statuscode 400: Invalid input options
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|
||||||
@@ -65,16 +65,11 @@ invoice_address object Invoice address
|
|||||||
├ state string Customer state (ISO 3166-2 code). Only supported in
|
├ state string Customer state (ISO 3166-2 code). Only supported in
|
||||||
AU, BR, CA, CN, MY, MX, and US.
|
AU, BR, CA, CN, MY, MX, and US.
|
||||||
├ internal_reference string Customer's internal reference to be printed on the invoice
|
├ internal_reference string Customer's internal reference to be printed on the invoice
|
||||||
|
|
||||||
├ custom_field string Custom invoice address field
|
├ custom_field string Custom invoice address field
|
||||||
├ vat_id string Customer VAT ID
|
├ vat_id string Customer VAT ID
|
||||||
├ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
||||||
EU VAT service and validation was successful. This only
|
EU VAT service and validation was successful. This only
|
||||||
happens in rare cases.
|
happens in rare cases.
|
||||||
├ transmission_type string Transmission channel for invoice (see also :ref:`rest-transmission-types`).
|
|
||||||
Defaults to ``email``.
|
|
||||||
└ transmission_info object Transmission-channel specific information (or ``null``).
|
|
||||||
See also :ref:`rest-transmission-types`.
|
|
||||||
positions list of objects List of order positions (see below). By default, only
|
positions list of objects List of order positions (see below). By default, only
|
||||||
non-canceled positions are included.
|
non-canceled positions are included.
|
||||||
fees list of objects List of fees included in the order total. By default, only
|
fees list of objects List of fees included in the order total. By default, only
|
||||||
@@ -147,10 +142,6 @@ plugin_data object Additional data
|
|||||||
|
|
||||||
The ``plugin_data`` attribute has been added.
|
The ``plugin_data`` attribute has been added.
|
||||||
|
|
||||||
.. versionchanged:: 2025.6
|
|
||||||
|
|
||||||
The ``invoice_address.transmission_type`` and ``invoice_address.transmission_info`` attributes have been added.
|
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
Order position resource
|
Order position resource
|
||||||
@@ -377,9 +368,7 @@ List of all orders
|
|||||||
"state": "",
|
"state": "",
|
||||||
"internal_reference": "",
|
"internal_reference": "",
|
||||||
"vat_id": "EU123456789",
|
"vat_id": "EU123456789",
|
||||||
"vat_id_validated": false,
|
"vat_id_validated": false
|
||||||
"transmission_type": "email",
|
|
||||||
"transmission_info": {}
|
|
||||||
},
|
},
|
||||||
"positions": [
|
"positions": [
|
||||||
{
|
{
|
||||||
@@ -418,7 +407,6 @@ List of all orders
|
|||||||
"seat": null,
|
"seat": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"id": 1337,
|
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
@@ -622,9 +610,7 @@ Fetching individual orders
|
|||||||
"state": "",
|
"state": "",
|
||||||
"internal_reference": "",
|
"internal_reference": "",
|
||||||
"vat_id": "EU123456789",
|
"vat_id": "EU123456789",
|
||||||
"vat_id_validated": false,
|
"vat_id_validated": false
|
||||||
"transmission_type": "email",
|
|
||||||
"transmission_info": {}
|
|
||||||
},
|
},
|
||||||
"positions": [
|
"positions": [
|
||||||
{
|
{
|
||||||
@@ -663,7 +649,6 @@ Fetching individual orders
|
|||||||
"seat": null,
|
"seat": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"id": 1337,
|
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
@@ -1030,10 +1015,8 @@ Creating orders
|
|||||||
* ``internal_reference``
|
* ``internal_reference``
|
||||||
* ``vat_id``
|
* ``vat_id``
|
||||||
* ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check
|
* ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check
|
||||||
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
|
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
|
||||||
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
||||||
* ``transmission_type`` (optional, defaults to ``email``)
|
|
||||||
* ``transmission_info`` (optional, see also :ref:`rest-transmission-types`)
|
|
||||||
|
|
||||||
* ``positions``
|
* ``positions``
|
||||||
|
|
||||||
@@ -1058,7 +1041,6 @@ Creating orders
|
|||||||
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
||||||
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
|
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
|
||||||
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
|
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
|
||||||
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
|
|
||||||
* ``answers``
|
* ``answers``
|
||||||
|
|
||||||
* ``question``
|
* ``question``
|
||||||
@@ -1635,7 +1617,6 @@ List of all order positions
|
|||||||
"blocked": null,
|
"blocked": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"id": 1337,
|
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
@@ -1764,7 +1745,6 @@ Fetching individual positions
|
|||||||
"seat": null,
|
"seat": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"id": 1337,
|
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
@@ -1946,7 +1926,6 @@ Manipulating individual positions
|
|||||||
|
|
||||||
(Full order position resource, see above.)
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
:query boolean check_quotas: Whether to check quotas before committing item changes, default is ``true``
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
:param event: The ``slug`` field of the event
|
:param event: The ``slug`` field of the event
|
||||||
:param id: The ``id`` field of the order position to update
|
:param id: The ``id`` field of the order position to update
|
||||||
@@ -2026,7 +2005,6 @@ Manipulating individual positions
|
|||||||
|
|
||||||
(Full order position resource, see above.)
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
:query boolean check_quotas: Whether to check quotas before creating the new position, default is ``true``
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
:param event: The ``slug`` field of the event
|
:param event: The ``slug`` field of the event
|
||||||
|
|
||||||
@@ -2313,7 +2291,6 @@ otherwise, such as splitting an order or changing fees.
|
|||||||
|
|
||||||
(Full order position resource, see above.)
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
:query boolean check_quotas: Whether to check quotas before patching or creating positions, default is ``true``
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
:param event: The ``slug`` field of the event
|
:param event: The ``slug`` field of the event
|
||||||
:param code: The ``code`` field of the order to update
|
:param code: The ``code`` field of the order to update
|
||||||
@@ -2509,7 +2486,6 @@ Order payment endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"amount": "23.00",
|
"amount": "23.00",
|
||||||
"comment": "Overpayment",
|
|
||||||
"mark_canceled": false
|
"mark_canceled": false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,6 @@ name string The organizer's
|
|||||||
slug string A short form of the name, used e.g. in URLs.
|
slug string A short form of the name, used e.g. in URLs.
|
||||||
public_url string The public, customer-facing URL of the organizer, where
|
public_url string The public, customer-facing URL of the organizer, where
|
||||||
the list of all events can be found (read-only).
|
the list of all events can be found (read-only).
|
||||||
plugins list A list of package names of the enabled plugins for this
|
|
||||||
organizer. Note that most plugins are enabled on the
|
|
||||||
event level (or both levels). If you remove a plugin
|
|
||||||
that is also enabled on some events, it will
|
|
||||||
automatically be removed from all events as well.
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -58,10 +53,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"name": "Big Events LLC",
|
"name": "Big Events LLC",
|
||||||
"slug": "Big Events",
|
"slug": "Big Events",
|
||||||
"public_url": "https://pretix.eu/bigevents/",
|
"public_url": "https://pretix.eu/bigevents/"
|
||||||
"plugins": [
|
|
||||||
"pretix_datev"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -95,10 +87,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"name": "Big Events LLC",
|
"name": "Big Events LLC",
|
||||||
"slug": "Big Events",
|
"slug": "Big Events",
|
||||||
"public_url": "https://pretix.eu/bigevents/",
|
"public_url": "https://pretix.eu/bigevents/"
|
||||||
"plugins": [
|
|
||||||
"pretix_datev"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -106,50 +95,6 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/
|
|
||||||
|
|
||||||
Updates an organizer. Currently only the ``plugins`` field may be updated.
|
|
||||||
|
|
||||||
Permission required: "Can change organizer settings"
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
PATCH /api/v1/organizers/bigevents/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"plugins": [
|
|
||||||
"pretix_seating"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Big Events LLC",
|
|
||||||
"slug": "Big Events",
|
|
||||||
"public_url": "https://pretix.eu/bigevents/",
|
|
||||||
"plugins": [
|
|
||||||
"pretix_seating"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to update
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 400: The organizer could not be updated due to invalid submitted data.
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to update this resource.
|
|
||||||
|
|
||||||
Organizer settings
|
Organizer settings
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ closed boolean Whether the quo
|
|||||||
field).
|
field).
|
||||||
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
|
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
|
||||||
have been scanned at an exit.
|
have been scanned at an exit.
|
||||||
ignore_for_event_availability boolean Whether the quota is ignored when calculating the event's
|
|
||||||
availability of tickets.
|
|
||||||
available boolean Whether this quota is available. Only returned if ``with_availability=true``
|
available boolean Whether this quota is available. Only returned if ``with_availability=true``
|
||||||
is set on the request. Do not rely on this value for critical operations, it may be
|
is set on the request. Do not rely on this value for critical operations, it may be
|
||||||
slightly out of date.
|
slightly out of date.
|
||||||
@@ -38,10 +36,6 @@ available_number integer Number of avail
|
|||||||
slightly out of date. ``null`` means unlimited.
|
slightly out of date. ``null`` means unlimited.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 2025.7
|
|
||||||
|
|
||||||
The attribute ``ignore_for_event_availability`` has been added.
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -78,8 +72,7 @@ Endpoints
|
|||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false,
|
"closed": false
|
||||||
"ignore_for_event_availability": false
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -125,8 +118,7 @@ Endpoints
|
|||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false,
|
"closed": false
|
||||||
"ignore_for_event_availability": false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -157,8 +149,7 @@ Endpoints
|
|||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false,
|
"closed": false
|
||||||
"ignore_for_event_availability": false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -177,8 +168,7 @@ Endpoints
|
|||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false,
|
"closed": false
|
||||||
"ignore_for_event_availability": false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a quota for
|
:param organizer: The ``slug`` field of the organizer of the event/item to create a quota for
|
||||||
@@ -233,8 +223,7 @@ Endpoints
|
|||||||
],
|
],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false,
|
"closed": false
|
||||||
"ignore_for_event_availability": false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
.. _rest-transactions:
|
|
||||||
|
|
||||||
Transactions
|
|
||||||
============
|
|
||||||
|
|
||||||
Transactions are an additional way to think about orders. They are are an immutable, filterable view into an order's
|
|
||||||
history and are a good basis for financial reporting.
|
|
||||||
|
|
||||||
Our financial model
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
You can think of a pretix order similar to a debtor account in double-entry bookkeeping. For example, the flow of an
|
|
||||||
order could look like this:
|
|
||||||
|
|
||||||
===================================================== ==================== =====================
|
|
||||||
Transaction Debit Credit
|
|
||||||
===================================================== ==================== =====================
|
|
||||||
Order is placed with two tickets € 500
|
|
||||||
Order is paid partially with a gift card € 200
|
|
||||||
Remainder is paid with a credit card € 300
|
|
||||||
One of the tickets is canceled **-** € 250
|
|
||||||
Refund is made to the credit card **-** € 250
|
|
||||||
**Balance** **€ 250** **€ 250**
|
|
||||||
===================================================== ==================== =====================
|
|
||||||
|
|
||||||
If an order is fully settled, the sums of both columns match. However, as the movements in both columns do not always
|
|
||||||
happen at the same time, at some times during the lifecycle of an order the sums are not balanced, in which case we
|
|
||||||
consider an order to be "pending payment" or "overpaid".
|
|
||||||
|
|
||||||
In the API, the "Debit" column is represented by the "transaction" resource listed on this page.
|
|
||||||
In many cases, the left column *usually* also matches the data returned by the :ref:`rest-invoices` resource, but there
|
|
||||||
are two important differences:
|
|
||||||
|
|
||||||
- pretix may be configured such that an invoice is not always generated for an order. In this case, only the transactions
|
|
||||||
return the full data set.
|
|
||||||
|
|
||||||
- pretix does not enforce a new invoice to be created e.g. when a ticket is changed to a different subevent. However,
|
|
||||||
pretix always creates a new transaction whenever there is a change to a ticket that concerns the **price**, **tax rate**,
|
|
||||||
**product**, or **date** (in an event series).
|
|
||||||
|
|
||||||
The :ref:`rest-orders` themselves are not a good representation of the "Debit" side of the table for accounting
|
|
||||||
purposes since they are not immutable:
|
|
||||||
They will only tell you the current state of the order, not what it was a week ago.
|
|
||||||
|
|
||||||
The "Credit" column is represented by the :ref:`order-payment-resource` and :ref:`order-refund-resource`.
|
|
||||||
|
|
||||||
|
|
||||||
Resource description
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
id integer Internal ID of the transaction
|
|
||||||
order string Order code the transaction was created from
|
|
||||||
event string Event slug, only present on organizer-level API calls
|
|
||||||
created datetime The creation time of the transaction in the database
|
|
||||||
datetime datetime The time at which the transaction is financially relevant.
|
|
||||||
This is usually the same as created, but may vary for
|
|
||||||
retroactively created transactions after software bugs or
|
|
||||||
for data that preceeds this data model.
|
|
||||||
positionid integer Number of the position within the order this refers to,
|
|
||||||
is ``null`` for transactions that refer to a fee
|
|
||||||
count integer Number of items purchased, is negative for cancellations
|
|
||||||
item integer The internal ID of the item purchased (or ``null`` for fees)
|
|
||||||
variation integer The internal ID of the variation purchased (or ``null``)
|
|
||||||
subevent integer The internal ID of the event series date (or ``null``)
|
|
||||||
price money (string) Gross price of the transaction
|
|
||||||
tax_rate decimal (string) Tax rate applied in transaction
|
|
||||||
tax_rule integer The internal ID of the tax rule used (or ``null``)
|
|
||||||
tax_code string The selected tax code (or ``null``)
|
|
||||||
tax_value money (string) The computed tax value
|
|
||||||
fee_type string The type of fee (or ``null`` for products)
|
|
||||||
internal_type string Additional type classification of the fee (or ``null`` for products)
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
.. versionchanged:: 2025.7.0
|
|
||||||
|
|
||||||
This resource was added to the API.
|
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/transactions/
|
|
||||||
|
|
||||||
Returns a list of all transactions of an event.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/sampleconf/transactions/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"count": 1,
|
|
||||||
"next": null,
|
|
||||||
"previous": null,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"order": "FOO",
|
|
||||||
"count": 1,
|
|
||||||
"created": "2017-12-01T10:00:00Z",
|
|
||||||
"datetime": "2017-12-01T10:00:00Z",
|
|
||||||
"item": null,
|
|
||||||
"variation": null,
|
|
||||||
"positionid": 1,
|
|
||||||
"price": "23.00",
|
|
||||||
"subevent": null,
|
|
||||||
"tax_code": "E",
|
|
||||||
"tax_rate": "0.00",
|
|
||||||
"tax_rule": 23,
|
|
||||||
"tax_value": "0.00",
|
|
||||||
"fee_type": null,
|
|
||||||
"internal_type": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
|
||||||
:query string order: Only return transactions matching the given order code.
|
|
||||||
:query datetime_since: Only return transactions with a datetime at or after the given time.
|
|
||||||
:query datetime_before: Only return transactions with a datetime before the given time.
|
|
||||||
:query created_since: Only return transactions with a creation time at or after the given time.
|
|
||||||
:query created_before: Only return transactions with a creation time before the given time.
|
|
||||||
:query item: Only return transactions that match the given item ID.
|
|
||||||
:query item__in: Only return transactions that match one of the given item IDs (separated with a comma).
|
|
||||||
:query variation: Only return transactions that match the given variation ID.
|
|
||||||
:query variation__in: Only return transactions that match one of the given variation IDs (separated with a comma).
|
|
||||||
:query subevent: Only return transactions that match the given subevent ID.
|
|
||||||
:query subevent__in: Only return transactions that match one of the given subevent IDs (separated with a comma).
|
|
||||||
:query tax_rule: Only return transactions that match the given tax rule ID.
|
|
||||||
:query tax_rule__in: Only return transactions that match one of the given tax rule IDs (separated with a comma).
|
|
||||||
:query tax_code: Only return transactions that match the given tax code.
|
|
||||||
:query tax_code__in: Only return transactions that match one of the given tax codes (separated with a comma).
|
|
||||||
:query tax_rate: Only return transactions that match the given tax rate.
|
|
||||||
:query tax_rate__in: Only return transactions that match one of the given tax rates (separated with a comma).
|
|
||||||
:query fee_type: Only return transactions that match the given fee type.
|
|
||||||
:query fee_type__in: Only return transactions that match one of the given fee types (separated with a comma).
|
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``created``, and ``id``.
|
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
|
||||||
:param event: The ``slug`` field of a valid event
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/transactions/
|
|
||||||
|
|
||||||
Returns a list of all transactions of an organizer that you have access to.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/transactions/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"count": 1,
|
|
||||||
"next": null,
|
|
||||||
"previous": null,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"event": "sampleconf",
|
|
||||||
"order": "FOO",
|
|
||||||
"count": 1,
|
|
||||||
"created": "2017-12-01T10:00:00Z",
|
|
||||||
"datetime": "2017-12-01T10:00:00Z",
|
|
||||||
"item": null,
|
|
||||||
"variation": null,
|
|
||||||
"positionid": 1,
|
|
||||||
"price": "23.00",
|
|
||||||
"subevent": null,
|
|
||||||
"tax_code": "E",
|
|
||||||
"tax_rate": "0.00",
|
|
||||||
"tax_rule": 23,
|
|
||||||
"tax_value": "0.00",
|
|
||||||
"fee_type": null,
|
|
||||||
"internal_type": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
|
||||||
:query string event: Only return transactions matching the given event slug.
|
|
||||||
:query string order: Only return transactions matching the given order code.
|
|
||||||
:query datetime_since: Only return transactions with a datetime at or after the given time.
|
|
||||||
:query datetime_before: Only return transactions with a datetime before the given time.
|
|
||||||
:query created_since: Only return transactions with a creation time at or after the given time.
|
|
||||||
:query created_before: Only return transactions with a creation time before the given time.
|
|
||||||
:query item: Only return transactions that match the given item ID.
|
|
||||||
:query item__in: Only return transactions that match one of the given item IDs (separated with a comma).
|
|
||||||
:query variation: Only return transactions that match the given variation ID.
|
|
||||||
:query variation__in: Only return transactions that match one of the given variation IDs (separated with a comma).
|
|
||||||
:query subevent: Only return transactions that match the given subevent ID.
|
|
||||||
:query subevent__in: Only return transactions that match one of the given subevent IDs (separated with a comma).
|
|
||||||
:query tax_rule: Only return transactions that match the given tax rule ID.
|
|
||||||
:query tax_rule__in: Only return transactions that match one of the given tax rule IDs (separated with a comma).
|
|
||||||
:query tax_code: Only return transactions that match the given tax code.
|
|
||||||
:query tax_code__in: Only return transactions that match one of the given tax codes (separated with a comma).
|
|
||||||
:query tax_rate: Only return transactions that match the given tax rate.
|
|
||||||
:query tax_rate__in: Only return transactions that match one of the given tax rates (separated with a comma).
|
|
||||||
:query fee_type: Only return transactions that match the given fee type.
|
|
||||||
:query fee_type__in: Only return transactions that match one of the given fee types (separated with a comma).
|
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``created``, and ``id``.
|
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
|
||||||
@@ -14,7 +14,6 @@ The voucher resource contains the following public fields:
|
|||||||
Field Type Description
|
Field Type Description
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
id integer Internal ID of the voucher
|
id integer Internal ID of the voucher
|
||||||
created datetime The creation date of the voucher. For vouchers created before pretix 2025.7.0, this is guessed retroactively and might not be accurate.
|
|
||||||
code string The voucher code that is required to redeem the voucher
|
code string The voucher code that is required to redeem the voucher
|
||||||
max_usages integer The maximum number of times this voucher can be
|
max_usages integer The maximum number of times this voucher can be
|
||||||
redeemed (default: 1).
|
redeemed (default: 1).
|
||||||
@@ -50,14 +49,8 @@ subevent integer ID of the date
|
|||||||
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
||||||
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
|
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
|
||||||
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
|
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
|
||||||
budget money (string) The budget a voucher is allowed to consume before being used up (or ``null``)
|
|
||||||
budget_used money (string) The amount of budget the voucher has already used up.
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 2025.7
|
|
||||||
|
|
||||||
The attributes ``created``, ``budget``, and ``budget_used`` have been added.
|
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
@@ -89,7 +82,6 @@ Endpoints
|
|||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created": "2020-09-18T14:17:40.971519Z",
|
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
"max_usages": 1,
|
"max_usages": 1,
|
||||||
"redeemed": 0,
|
"redeemed": 0,
|
||||||
@@ -107,9 +99,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"show_hidden_items": false,
|
"show_hidden_items": false,
|
||||||
"all_addons_included": false,
|
"all_addons_included": false,
|
||||||
"all_bundles_included": false,
|
"all_bundles_included": false
|
||||||
"budget": None,
|
|
||||||
"budget_used": "0.00"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -162,7 +152,6 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created": "2020-09-18T14:17:40.971519Z",
|
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
"max_usages": 1,
|
"max_usages": 1,
|
||||||
"redeemed": 0,
|
"redeemed": 0,
|
||||||
@@ -180,9 +169,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"show_hidden_items": false,
|
"show_hidden_items": false,
|
||||||
"all_addons_included": false,
|
"all_addons_included": false,
|
||||||
"all_bundles_included": false,
|
"all_bundles_included": false
|
||||||
"budget": None,
|
|
||||||
"budget_used": "0.00"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -235,7 +222,6 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created": "2020-09-18T14:17:40.971519Z",
|
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
"max_usages": 1,
|
"max_usages": 1,
|
||||||
"redeemed": 0,
|
"redeemed": 0,
|
||||||
@@ -253,9 +239,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"show_hidden_items": false,
|
"show_hidden_items": false,
|
||||||
"all_addons_included": false,
|
"all_addons_included": false,
|
||||||
"all_bundles_included": false,
|
"all_bundles_included": false
|
||||||
"budget": None,
|
|
||||||
"budget_used": "0.00"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to create a voucher for
|
:param organizer: The ``slug`` field of the organizer to create a voucher for
|
||||||
@@ -329,7 +313,6 @@ Endpoints
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created": "2020-09-18T14:17:40.971519Z",
|
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
…
|
…
|
||||||
}, …
|
}, …
|
||||||
@@ -376,7 +359,6 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created": "2020-09-18T14:17:40.971519Z",
|
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
"max_usages": 1,
|
"max_usages": 1,
|
||||||
"redeemed": 0,
|
"redeemed": 0,
|
||||||
@@ -394,9 +376,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"show_hidden_items": false,
|
"show_hidden_items": false,
|
||||||
"all_addons_included": false,
|
"all_addons_included": false,
|
||||||
"all_bundles_included": false,
|
"all_bundles_included": false
|
||||||
"budget": None,
|
|
||||||
"budget_used": "0.00"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -60,9 +60,6 @@ The following values for ``action_types`` are valid with pretix core:
|
|||||||
* ``pretix.event.added``
|
* ``pretix.event.added``
|
||||||
* ``pretix.event.changed``
|
* ``pretix.event.changed``
|
||||||
* ``pretix.event.deleted``
|
* ``pretix.event.deleted``
|
||||||
* ``pretix.voucher.added``
|
|
||||||
* ``pretix.voucher.changed``
|
|
||||||
* ``pretix.voucher.deleted``
|
|
||||||
* ``pretix.subevent.added``
|
* ``pretix.subevent.added``
|
||||||
* ``pretix.subevent.changed``
|
* ``pretix.subevent.changed``
|
||||||
* ``pretix.subevent.deleted``
|
* ``pretix.subevent.deleted``
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
.. highlight:: python
|
|
||||||
:linenothreshold: 5
|
|
||||||
|
|
||||||
Data sync providers
|
|
||||||
===================
|
|
||||||
|
|
||||||
.. warning:: This feature is considered **experimental**. It might change at any time without prior notice.
|
|
||||||
|
|
||||||
pretix provides connectivity to many external services through plugins. A common requirement
|
|
||||||
is unidirectionally sending (order, customer, ticket, ...) data into external systems.
|
|
||||||
The transfer is usually triggered by signals provided by pretix core (e.g. :data:`order_placed`),
|
|
||||||
but performed asynchronously.
|
|
||||||
|
|
||||||
Such plugins should use the :class:`OutboundSyncProvider` API to utilize the queueing, retry and mapping
|
|
||||||
mechanisms as well as the user interface for configuration and monitoring. Sync providers are registered
|
|
||||||
in the :py:attr:`pretix.base.datasync.datasync.datasync_providers` :ref:`registry <registries>`.
|
|
||||||
|
|
||||||
An :class:`OutboundSyncProvider` for subscribing event participants to a mailing list could start
|
|
||||||
like this, for example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from pretix.base.datasync.datasync import (OutboundSyncProvider, datasync_providers)
|
|
||||||
|
|
||||||
@datasync_providers.register
|
|
||||||
class MyListSyncProvider(OutboundSyncProvider):
|
|
||||||
identifier = "my_list"
|
|
||||||
display_name = "My Mailing List Service"
|
|
||||||
# ...
|
|
||||||
|
|
||||||
|
|
||||||
The plugin must register listeners in `signals.py` for all signals that should to trigger a sync and
|
|
||||||
within it has to call :meth:`MyListSyncProvider.enqueue_order` to enqueue the order for synchronization:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@receiver(order_placed, dispatch_uid="mylist_order_placed")
|
|
||||||
def on_order_placed(sender, order, **kwargs):
|
|
||||||
MyListSyncProvider.enqueue_order(order, "order_placed")
|
|
||||||
|
|
||||||
|
|
||||||
Property mappings
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
Most of these plugins need to translate data from some pretix objects (e.g. orders)
|
|
||||||
into an external system's data structures. Sometimes, there is only one reasonable way or the
|
|
||||||
plugin author makes an opinionated decision what information from which objects should be
|
|
||||||
transferred into which data structures in the external system.
|
|
||||||
|
|
||||||
Otherwise, you can use a :class:`PropertyMappingFormSet` to let the user set up a mapping from pretix model fields
|
|
||||||
to external data fields. You could store the mapping information either in the event settings, or in a separate
|
|
||||||
data model. Your implementation of :attr:`OutboundSyncProvider.mappings`
|
|
||||||
needs to provide a list of mappings, which can be e.g. static objects or model instances, as long as they
|
|
||||||
have at least the properties defined in
|
|
||||||
:class:`pretix.base.datasync.datasync.StaticMapping`.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# class MyListSyncProvider, contd.
|
|
||||||
def mappings(self):
|
|
||||||
return [
|
|
||||||
StaticMapping(
|
|
||||||
id=1, pretix_model='Order', external_object_type='Contact',
|
|
||||||
pretix_id_field='email', external_id_field='email',
|
|
||||||
property_mappings=self.event.settings.mylist_order_mapping,
|
|
||||||
))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
Currently, we support `orders` and `order positions` as data sources, with the data fields defined in
|
|
||||||
:func:`pretix.base.datasync.sourcefields.get_data_fields`.
|
|
||||||
|
|
||||||
To perform the actual sync, implement :func:`sync_object_with_properties` and optionally
|
|
||||||
:func:`finalize_sync_order`. The former is called for each object to be created according to the ``mappings``.
|
|
||||||
For each order that was enqueued using :func:`enqueue_order`:
|
|
||||||
|
|
||||||
- each Mapping with ``pretix_model == "Order"`` results in one call to :func:`sync_object_with_properties`,
|
|
||||||
- each Mapping with ``pretix_model == "OrderPosition"`` results in one call to
|
|
||||||
:func:`sync_object_with_properties` per order position,
|
|
||||||
- :func:`finalize_sync_order` is called one time after all calls to :func:`sync_object_with_properties`.
|
|
||||||
|
|
||||||
|
|
||||||
Implementation examples
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
For example implementations, see the test cases in :mod:`tests.base.test_datasync`.
|
|
||||||
|
|
||||||
In :class:`SimpleOrderSync`, a basic data transfer of order data only is
|
|
||||||
shown. Therein, a ``sync_object_with_properties`` method is defined as follows:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from pretix.base.datasync.utils import assign_properties
|
|
||||||
|
|
||||||
# class MyListSyncProvider, contd.
|
|
||||||
def sync_object_with_properties(
|
|
||||||
self, external_id_field, id_value, properties: list, inputs: dict,
|
|
||||||
mapping, mapped_objects: dict, **kwargs,
|
|
||||||
):
|
|
||||||
# First, we query the external service if our object-to-sync already exists there.
|
|
||||||
# This is necessary to make sure our method is idempotent, i.e. handles already synced
|
|
||||||
# data gracefully.
|
|
||||||
pre_existing_object = self.fake_api_client.retrieve_object(
|
|
||||||
mapping.external_object_type,
|
|
||||||
external_id_field,
|
|
||||||
id_value
|
|
||||||
)
|
|
||||||
|
|
||||||
# We use the helper function ``assign_properties`` to update a pre-existing object.
|
|
||||||
update_values = assign_properties(
|
|
||||||
new_values=properties,
|
|
||||||
old_values=pre_existing_object or {},
|
|
||||||
is_new=pre_existing_object is None,
|
|
||||||
list_sep=";",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Then we can send our new data to the external service. The specifics of course depends
|
|
||||||
# on your API, e.g. you may need to use different endpoints for creating or updating an
|
|
||||||
# object, or pass the identifier separately instead of in the same dictionary as the
|
|
||||||
# other properties.
|
|
||||||
result = self.fake_api_client.create_or_update_object(mapping.external_object_type, {
|
|
||||||
**update_values,
|
|
||||||
external_id_field: id_value,
|
|
||||||
"_id": pre_existing_object and pre_existing_object.get("_id"),
|
|
||||||
})
|
|
||||||
|
|
||||||
# Finally, return a dictionary containing at least `object_type`, `external_id_field`,
|
|
||||||
# `id_value`, `external_link_href`, and `external_link_display_name` keys.
|
|
||||||
# Further keys may be provided for your internal use. This dictionary is provided
|
|
||||||
# in following calls in the ``mapped_objects`` dict, to allow creating associations
|
|
||||||
# to this object.
|
|
||||||
return {
|
|
||||||
"object_type": mapping.external_object_type,
|
|
||||||
"external_id_field": external_id_field,
|
|
||||||
"id_value": id_value,
|
|
||||||
"external_link_href": f"https://example.org/external-system/{mapping.external_object_type}/{id_value}/",
|
|
||||||
"external_link_display_name": f"Contact #{id_value} - Jane Doe",
|
|
||||||
"my_result": result,
|
|
||||||
}
|
|
||||||
|
|
||||||
.. note:: The result dictionaries of earlier invocations of :func:`sync_object_with_properties` are
|
|
||||||
only provided in subsequent calls of the same sync run, such that a mapping can
|
|
||||||
refer to e.g. the external id of an object created by a preceding mapping.
|
|
||||||
However, the result dictionaries are currently not provided across runs. This will
|
|
||||||
likely change in a future revision of this API, to allow easier integration of external
|
|
||||||
systems that do not allow retrieving/updating data by a pretix-provided key.
|
|
||||||
|
|
||||||
``mapped_objects`` is a dictionary of lists of dictionaries. The keys to the dictionary are
|
|
||||||
the mapping identifiers (``mapping.id``), the lists contain the result dictionaries returned
|
|
||||||
by :func:`sync_object_with_properties`.
|
|
||||||
|
|
||||||
|
|
||||||
In :class:`OrderAndTicketAssociationSync`, an example is given where orders, order positions,
|
|
||||||
and the association between them are transferred.
|
|
||||||
|
|
||||||
|
|
||||||
The OutboundSyncProvider base class
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
.. autoclass:: pretix.base.datasync.datasync.OutboundSyncProvider
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
Property mapping format
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
To allow the user to configure property mappings, you can use the PropertyMappingFormSet,
|
|
||||||
which will generate the required ``property_mappings`` value automatically. If you need
|
|
||||||
to specify the property mappings programmatically, you can refer to the description below
|
|
||||||
on their format.
|
|
||||||
|
|
||||||
.. autoclass:: pretix.control.forms.mapping.PropertyMappingFormSet
|
|
||||||
:members: to_property_mappings_json
|
|
||||||
|
|
||||||
A simple JSON-serialized ``property_mappings`` list for mapping some order information can look like this:
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"pretix_field": "email",
|
|
||||||
"external_field": "orderemail",
|
|
||||||
"value_map": "",
|
|
||||||
"overwrite": "overwrite",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pretix_field": "order_status",
|
|
||||||
"external_field": "status",
|
|
||||||
"value_map": "{\"n\": \"pending\", \"p\": \"paid\", \"e\": \"expired\", \"c\": \"canceled\", \"r\": \"refunded\"}",
|
|
||||||
"overwrite": "overwrite",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pretix_field": "order_total",
|
|
||||||
"external_field": "total",
|
|
||||||
"value_map": "",
|
|
||||||
"overwrite": "overwrite",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
Translating mappings on Event copy
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
Property mappings can contain references to event-specific primary keys. Therefore, plugins must register to the
|
|
||||||
event_copy_data signal and call translate_property_mappings on all property mappings they store.
|
|
||||||
|
|
||||||
.. autofunction:: pretix.base.datasync.utils.translate_property_mappings
|
|
||||||
@@ -23,21 +23,21 @@ There are multiple signals that will be sent out in the ordering cycle:
|
|||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:no-index:
|
: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_expiry_changed, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, build_invoice_data, invoice_line_text
|
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_expiry_changed, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
||||||
|
|
||||||
Check-ins
|
Check-ins
|
||||||
"""""""""
|
"""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:no-index:
|
:no-index:
|
||||||
:members: checkin_created, checkin_annulled
|
:members: checkin_created
|
||||||
|
|
||||||
|
|
||||||
Frontend
|
Frontend
|
||||||
--------
|
--------
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head, filter_subevents
|
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
|||||||
@@ -13,12 +13,10 @@ Contents:
|
|||||||
email
|
email
|
||||||
placeholder
|
placeholder
|
||||||
invoice
|
invoice
|
||||||
invoicetransmission
|
|
||||||
shredder
|
shredder
|
||||||
import
|
import
|
||||||
customview
|
customview
|
||||||
cookieconsent
|
cookieconsent
|
||||||
auth
|
auth
|
||||||
datasync
|
|
||||||
general
|
general
|
||||||
quality
|
quality
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
.. highlight:: python
|
|
||||||
:linenothreshold: 5
|
|
||||||
|
|
||||||
Writing an invoice transmission plugin
|
|
||||||
======================================
|
|
||||||
|
|
||||||
An invoice transmission provider transports an invoice from the sender to the recipient.
|
|
||||||
There are pre-defined types of invoice transmission in pretix, currently ``"email"``, ``"peppol"``, and ``"it_sdi"``.
|
|
||||||
You can find more information about them at :ref:`rest-transmission-types`.
|
|
||||||
|
|
||||||
New transmission types can not be added by plugins but need to be added to pretix itself.
|
|
||||||
However, plugins can provide implementations for the actual transmission.
|
|
||||||
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
|
||||||
|
|
||||||
Output registration
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
New invoice transmission providers can be registered through the :ref:`registry <registries>` mechanism
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from pretix.base.invoicing.transmission import transmission_providers, TransmissionProvider
|
|
||||||
|
|
||||||
@transmission_providers.new()
|
|
||||||
class SdiTransmissionProvider(TransmissionProvider):
|
|
||||||
identifier = "fatturapa_providerabc"
|
|
||||||
type = "it_sdi"
|
|
||||||
verbose_name = _("FatturaPA through provider ABC")
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
The provider class
|
|
||||||
------------------
|
|
||||||
|
|
||||||
.. class:: pretix.base.invoicing.transmission.TransmissionProvider
|
|
||||||
|
|
||||||
.. autoattribute:: identifier
|
|
||||||
|
|
||||||
This is an abstract attribute, you **must** override this!
|
|
||||||
|
|
||||||
.. autoattribute:: type
|
|
||||||
|
|
||||||
This is an abstract attribute, you **must** override this!
|
|
||||||
|
|
||||||
.. autoattribute:: verbose_name
|
|
||||||
|
|
||||||
This is an abstract attribute, you **must** override this!
|
|
||||||
|
|
||||||
.. autoattribute:: priority
|
|
||||||
|
|
||||||
.. autoattribute:: testmode_supported
|
|
||||||
|
|
||||||
.. automethod:: is_ready
|
|
||||||
|
|
||||||
This is an abstract method, you **must** override this!
|
|
||||||
|
|
||||||
.. automethod:: is_available
|
|
||||||
|
|
||||||
This is an abstract method, you **must** override this!
|
|
||||||
|
|
||||||
.. automethod:: transmit
|
|
||||||
|
|
||||||
This is an abstract method, you **must** override this!
|
|
||||||
|
|
||||||
.. automethod:: settings_url
|
|
||||||
@@ -56,20 +56,6 @@ restricted boolean (optional) ``False`` by default, restricts a plugin
|
|||||||
for an event by system administrators / superusers.
|
for an event by system administrators / superusers.
|
||||||
experimental boolean (optional) ``False`` by default, marks a plugin as an experimental feature in the plugins list.
|
experimental boolean (optional) ``False`` by default, marks a plugin as an experimental feature in the plugins list.
|
||||||
compatibility string Specifier for compatible pretix versions.
|
compatibility string Specifier for compatible pretix versions.
|
||||||
level string System level the plugin can be activated at.
|
|
||||||
Set to ``pretix.base.plugins.PLUGIN_LEVEL_EVENT`` for plugins that can be activated
|
|
||||||
at event level and then be active for that event only.
|
|
||||||
Set to ``pretix.base.plugins.PLUGIN_LEVEL_ORGANIZER`` for plugins that can be
|
|
||||||
activated only for the organizer as a whole and are active for any event within
|
|
||||||
that organizer.
|
|
||||||
Set to ``pretix.base.plugins.PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID`` for plugins that
|
|
||||||
can be activated at organizer level but are considered active only within events
|
|
||||||
for which they have also been specifically activated.
|
|
||||||
More levels, e.g. user-level plugins, might be invented in the future.
|
|
||||||
settings_links list List of ``((menu name, submenu name, …), urlname, url_kwargs)`` tuples that point
|
|
||||||
to the plugin's settings.
|
|
||||||
navigation_links list List of ``((menu name, submenu name, …), urlname, url_kwargs)`` tuples that point
|
|
||||||
to the plugin's system pages.
|
|
||||||
================== ==================== ===========================================================
|
================== ==================== ===========================================================
|
||||||
|
|
||||||
A working example would be:
|
A working example would be:
|
||||||
@@ -77,9 +63,9 @@ A working example would be:
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pretix.base.plugins import PluginConfig, PLUGIN_LEVEL_EVENT
|
from pretix.base.plugins import PluginConfig
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise RuntimeError("Please use pretix 2025.7 or above to run this plugin!")
|
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
@@ -93,7 +79,6 @@ A working example would be:
|
|||||||
version = '1.0.0'
|
version = '1.0.0'
|
||||||
category = 'PAYMENT'
|
category = 'PAYMENT'
|
||||||
picture = 'pretix_paypal/paypal_logo.svg'
|
picture = 'pretix_paypal/paypal_logo.svg'
|
||||||
level = PLUGIN_LEVEL_EVENT
|
|
||||||
visible = True
|
visible = True
|
||||||
featured = False
|
featured = False
|
||||||
restricted = False
|
restricted = False
|
||||||
@@ -157,14 +142,14 @@ method to make your receivers available:
|
|||||||
from . import signals # NOQA
|
from . import signals # NOQA
|
||||||
|
|
||||||
You can optionally specify code that is executed when your plugin is activated for an event
|
You can optionally specify code that is executed when your plugin is activated for an event
|
||||||
or organizer in the ``installed`` method:
|
in the ``installed`` method:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class PaypalApp(AppConfig):
|
class PaypalApp(AppConfig):
|
||||||
…
|
…
|
||||||
|
|
||||||
def installed(self, event_or_organizer):
|
def installed(self, event):
|
||||||
pass # Your code here
|
pass # Your code here
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Development setup
|
|||||||
|
|
||||||
This tutorial helps you to get started hacking with pretix on your own computer. You need this to
|
This tutorial helps you to get started hacking with pretix on your own computer. You need this to
|
||||||
be able to contribute to pretix, but it might also be helpful if you want to write your own plugins.
|
be able to contribute to pretix, but it might also be helpful if you want to write your own plugins.
|
||||||
If you want to install pretix on a server for actual usage, go to the `administrator documentation`_ instead.
|
If you want to install pretix on a server for actual usage, go to the [administrator documentation](https://docs.pretix.eu/self-hosting/) instead.
|
||||||
|
|
||||||
Obtain a copy of the source code
|
Obtain a copy of the source code
|
||||||
--------------------------------
|
--------------------------------
|
||||||
@@ -221,4 +221,3 @@ your virtual environment.::
|
|||||||
|
|
||||||
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
||||||
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
||||||
.. _administrator documentation: https://docs.pretix.eu/self-hosting/
|
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ sphinxcontrib-images
|
|||||||
sphinxcontrib-jquery
|
sphinxcontrib-jquery
|
||||||
sphinxcontrib-spelling==8.*
|
sphinxcontrib-spelling==8.*
|
||||||
sphinxemoji
|
sphinxemoji
|
||||||
pyenchant==3.3.*
|
pyenchant==3.2.*
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ sphinxcontrib-images
|
|||||||
sphinxcontrib-jquery
|
sphinxcontrib-jquery
|
||||||
sphinxcontrib-spelling==8.*
|
sphinxcontrib-spelling==8.*
|
||||||
sphinxemoji
|
sphinxemoji
|
||||||
pyenchant==3.3.*
|
pyenchant==3.2.*
|
||||||
|
|||||||
@@ -28,23 +28,23 @@ classifiers = [
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
||||||
"babel",
|
"babel",
|
||||||
"BeautifulSoup4==4.14.*",
|
"BeautifulSoup4==4.13.*",
|
||||||
"bleach==6.2.*",
|
"bleach==6.2.*",
|
||||||
"celery==5.5.*",
|
"celery==5.5.*",
|
||||||
"chardet==5.2.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=44.0.0",
|
"cryptography>=44.0.0",
|
||||||
"css-inline==0.17.*",
|
"css-inline==0.15.*",
|
||||||
"defusedcsv>=1.1.0",
|
"defusedcsv>=1.1.0",
|
||||||
"Django[argon2]==4.2.*,>=4.2.24",
|
"Django[argon2]==4.2.*,>=4.2.15",
|
||||||
"django-bootstrap3==25.2",
|
"django-bootstrap3==25.1",
|
||||||
"django-compressor==4.5.1",
|
"django-compressor==4.5.1",
|
||||||
"django-countries==7.6.*",
|
"django-countries==7.6.*",
|
||||||
"django-filter==25.1",
|
"django-filter==25.1",
|
||||||
"django-formset-js-improved==0.5.0.4",
|
"django-formset-js-improved==0.5.0.3",
|
||||||
"django-formtools==2.5.1",
|
"django-formtools==2.5.1",
|
||||||
"django-hierarkey==2.0.*,>=2.0.1",
|
"django-hierarkey==1.2.*",
|
||||||
"django-hijack==3.7.*",
|
"django-hijack==3.7.*",
|
||||||
"django-i18nfield==1.11.*",
|
"django-i18nfield==1.10.*",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
"django-localflavor==5.0",
|
"django-localflavor==5.0",
|
||||||
"django-markup",
|
"django-markup",
|
||||||
@@ -64,7 +64,7 @@ dependencies = [
|
|||||||
"kombu==5.5.*",
|
"kombu==5.5.*",
|
||||||
"libsass==0.23.*",
|
"libsass==0.23.*",
|
||||||
"lxml",
|
"lxml",
|
||||||
"markdown==3.9", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
"markdown==3.8.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||||
"mt-940==4.30.*",
|
"mt-940==4.30.*",
|
||||||
"oauthlib==3.3.*",
|
"oauthlib==3.3.*",
|
||||||
@@ -74,25 +74,25 @@ dependencies = [
|
|||||||
"paypal-checkout-serversdk==1.0.*",
|
"paypal-checkout-serversdk==1.0.*",
|
||||||
"PyJWT==2.10.*",
|
"PyJWT==2.10.*",
|
||||||
"phonenumberslite==9.0.*",
|
"phonenumberslite==9.0.*",
|
||||||
"Pillow==11.3.*",
|
"Pillow==11.2.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==6.33.*",
|
"protobuf==6.31.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.23",
|
"pycparser==2.22",
|
||||||
"pycryptodome==3.23.*",
|
"pycryptodome==3.23.*",
|
||||||
"pypdf==6.1.*",
|
"pypdf==5.6.*",
|
||||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||||
"python-dateutil==2.9.*",
|
"python-dateutil==2.9.*",
|
||||||
"pytz",
|
"pytz",
|
||||||
"pytz-deprecation-shim==0.1.*",
|
"pytz-deprecation-shim==0.1.*",
|
||||||
"pyuca",
|
"pyuca",
|
||||||
"qrcode==8.2",
|
"qrcode==8.2",
|
||||||
"redis==6.4.*",
|
"redis==6.2.*",
|
||||||
"reportlab==4.4.*",
|
"reportlab==4.4.*",
|
||||||
"requests==2.32.*",
|
"requests==2.31.*",
|
||||||
"sentry-sdk==2.42.*",
|
"sentry-sdk==2.31.*",
|
||||||
"sepaxml==2.7.*",
|
"sepaxml==2.6.*",
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
"tlds>=2020041600",
|
"tlds>=2020041600",
|
||||||
@@ -100,29 +100,29 @@ dependencies = [
|
|||||||
"ua-parser==1.0.*",
|
"ua-parser==1.0.*",
|
||||||
"vat_moss_forked==2020.3.20.0.11.0",
|
"vat_moss_forked==2020.3.20.0.11.0",
|
||||||
"vobject==0.9.*",
|
"vobject==0.9.*",
|
||||||
"webauthn==2.7.*",
|
"webauthn==2.6.*",
|
||||||
"zeep==4.3.*"
|
"zeep==4.3.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memcached = ["pylibmc"]
|
memcached = ["pylibmc"]
|
||||||
dev = [
|
dev = [
|
||||||
"aiohttp==3.13.*",
|
"aiohttp==3.12.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.32.*",
|
"fakeredis==2.30.*",
|
||||||
"flake8==7.3.*",
|
"flake8==7.3.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==6.1.*",
|
"isort==6.0.*",
|
||||||
"pep8-naming==0.15.*",
|
"pep8-naming==0.15.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pytest-asyncio>=0.24",
|
"pytest-asyncio>=0.24",
|
||||||
"pytest-cache",
|
"pytest-cache",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-django==4.*",
|
"pytest-django==4.*",
|
||||||
"pytest-mock==3.15.*",
|
"pytest-mock==3.14.*",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.8.*",
|
"pytest-xdist==3.7.*",
|
||||||
"pytest==8.4.*",
|
"pytest==8.4.*",
|
||||||
"responses",
|
"responses",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ coverage:
|
|||||||
coverage run -m py.test
|
coverage run -m py.test
|
||||||
|
|
||||||
npminstall:
|
npminstall:
|
||||||
# keep this in sync with pretix/_build.py!
|
# keep this in sync with setup.py!
|
||||||
mkdir -p pretix/static.dist/node_prefix/
|
mkdir -p pretix/static.dist/node_prefix/
|
||||||
cp -r pretix/static/npm_dir/* pretix/static.dist/node_prefix/
|
cp -r pretix/static/npm_dir/* pretix/static.dist/node_prefix/
|
||||||
npm ci --prefix=pretix/static.dist/node_prefix
|
npm install --prefix=pretix/static.dist/node_prefix
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2025.9.0"
|
__version__ = "2025.7.0.dev0"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -115,7 +115,6 @@ ALL_LANGUAGES = [
|
|||||||
('sk', _('Slovak')),
|
('sk', _('Slovak')),
|
||||||
('sv', _('Swedish')),
|
('sv', _('Swedish')),
|
||||||
('es', _('Spanish')),
|
('es', _('Spanish')),
|
||||||
('es-419', _('Spanish (Latin America)')),
|
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
('uk', _('Ukrainian')),
|
('uk', _('Ukrainian')),
|
||||||
]
|
]
|
||||||
@@ -173,12 +172,6 @@ EXTRA_LANG_INFO = {
|
|||||||
'name': 'Norwegian Bokmal',
|
'name': 'Norwegian Bokmal',
|
||||||
'name_local': 'norsk (bokmål)',
|
'name_local': 'norsk (bokmål)',
|
||||||
},
|
},
|
||||||
'es-419': {
|
|
||||||
'bidi': False,
|
|
||||||
'code': 'es-419',
|
|
||||||
'name': 'Spanish (Latin America)',
|
|
||||||
'name_local': 'Español',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
|
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -39,7 +39,7 @@ def npm_install():
|
|||||||
node_prefix = os.path.join(here, 'static.dist', 'node_prefix')
|
node_prefix = os.path.join(here, 'static.dist', 'node_prefix')
|
||||||
os.makedirs(node_prefix, exist_ok=True)
|
os.makedirs(node_prefix, exist_ok=True)
|
||||||
shutil.copytree(os.path.join(here, 'static', 'npm_dir'), node_prefix, dirs_exist_ok=True)
|
shutil.copytree(os.path.join(here, 'static', 'npm_dir'), node_prefix, dirs_exist_ok=True)
|
||||||
subprocess.check_call('npm ci', shell=True, cwd=node_prefix)
|
subprocess.check_call('npm install', shell=True, cwd=node_prefix)
|
||||||
npm_installed = True
|
npm_installed = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -104,14 +104,3 @@ class MiniCheckinListSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class CheckinRPCAnnulInputSerializer(serializers.Serializer):
|
|
||||||
lists = serializers.PrimaryKeyRelatedField(required=True, many=True, queryset=CheckinList.objects.none())
|
|
||||||
nonce = serializers.CharField(required=True, allow_null=False)
|
|
||||||
datetime = serializers.DateTimeField(required=False, allow_null=True)
|
|
||||||
error_explanation = serializers.CharField(required=False, allow_null=True)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields['lists'].child_relation.queryset = CheckinList.objects.filter(event__in=self.context['events']).select_related('event')
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -50,7 +50,6 @@ from rest_framework.relations import SlugRelatedField
|
|||||||
from pretix.api.serializers import (
|
from pretix.api.serializers import (
|
||||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.fields import PluginsField
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -62,9 +61,6 @@ from pretix.base.models.items import (
|
|||||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||||
)
|
)
|
||||||
from pretix.base.models.tax import CustomRulesValidator
|
from pretix.base.models.tax import CustomRulesValidator
|
||||||
from pretix.base.plugins import (
|
|
||||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
|
||||||
)
|
|
||||||
from pretix.base.services.seating import (
|
from pretix.base.services.seating import (
|
||||||
SeatProtected, generate_seats, validate_plan_change,
|
SeatProtected, generate_seats, validate_plan_change,
|
||||||
)
|
)
|
||||||
@@ -130,6 +126,22 @@ class SeatCategoryMappingField(Field):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PluginsField(Field):
|
||||||
|
|
||||||
|
def to_representation(self, obj):
|
||||||
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
|
return sorted([
|
||||||
|
p.module for p in get_all_plugins()
|
||||||
|
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
|
||||||
|
])
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return {
|
||||||
|
'plugins': data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TimeZoneField(ChoiceField):
|
class TimeZoneField(ChoiceField):
|
||||||
def get_attribute(self, instance):
|
def get_attribute(self, instance):
|
||||||
return instance.cache.get_or_set(
|
return instance.cache.get_or_set(
|
||||||
@@ -271,28 +283,17 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
from pretix.base.plugins import get_all_plugins
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
plugins_available = {
|
plugins_available = {
|
||||||
p.module: p for p in get_all_plugins(event=self.instance)
|
p.module: p for p in get_all_plugins(self.instance)
|
||||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||||
}
|
}
|
||||||
current_plugins = self.instance.get_plugins() if self.instance and self.instance.pk else []
|
|
||||||
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
|
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
|
||||||
|
|
||||||
allowed_levels = (PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID)
|
|
||||||
for plugin in value.get('plugins'):
|
for plugin in value.get('plugins'):
|
||||||
if plugin not in plugins_available:
|
if plugin not in plugins_available:
|
||||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
||||||
if getattr(plugins_available[plugin], 'restricted', False):
|
if getattr(plugins_available[plugin], 'restricted', False):
|
||||||
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
||||||
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
||||||
level = getattr(plugins_available[plugin], 'level', PLUGIN_LEVEL_EVENT)
|
|
||||||
if level not in allowed_levels:
|
|
||||||
raise ValidationError('Plugin cannot be enabled on this level: \'{name}\'.'.format(name=plugin))
|
|
||||||
|
|
||||||
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID and plugin not in self.context['organizer'].get_plugins():
|
|
||||||
if plugin not in current_plugins:
|
|
||||||
# Technically, this is allowed, but consumers might be confused if the API call doesn't do anything
|
|
||||||
# so we prevent this change.
|
|
||||||
raise ValidationError('Plugin should be enabled on organizer level first: \'{name}\'.'.format(name=plugin))
|
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -805,7 +806,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'invoice_reissue_after_modify',
|
'invoice_reissue_after_modify',
|
||||||
'invoice_include_free',
|
'invoice_include_free',
|
||||||
'invoice_generate',
|
'invoice_generate',
|
||||||
'invoice_period',
|
|
||||||
'invoice_numbers_consecutive',
|
'invoice_numbers_consecutive',
|
||||||
'invoice_numbers_prefix',
|
'invoice_numbers_prefix',
|
||||||
'invoice_numbers_prefix_cancellations',
|
'invoice_numbers_prefix_cancellations',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,16 +19,45 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from pretix.api.serializers.forms import form_field_to_serializer_field
|
|
||||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||||
from pretix.base.models import ScheduledEventExport, ScheduledOrganizerExport
|
from pretix.base.models import ScheduledEventExport, ScheduledOrganizerExport
|
||||||
from pretix.base.timeframes import SerializerDateFrameField
|
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
|
||||||
|
|
||||||
|
|
||||||
|
class FormFieldWrapperField(serializers.Field):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.form_field = kwargs.pop('form_field')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return self.form_field.widget.format_value(value)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
d = self.form_field.widget.value_from_datadict({'name': data}, {}, 'name')
|
||||||
|
d = self.form_field.clean(d)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
simple_mappings = (
|
||||||
|
(forms.DateField, serializers.DateField, ()),
|
||||||
|
(forms.TimeField, serializers.TimeField, ()),
|
||||||
|
(forms.SplitDateTimeField, serializers.DateTimeField, ()),
|
||||||
|
(forms.DateTimeField, serializers.DateTimeField, ()),
|
||||||
|
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
|
||||||
|
(forms.FloatField, serializers.FloatField, ()),
|
||||||
|
(forms.IntegerField, serializers.IntegerField, ()),
|
||||||
|
(forms.EmailField, serializers.EmailField, ()),
|
||||||
|
(forms.UUIDField, serializers.UUIDField, ()),
|
||||||
|
(forms.URLField, serializers.URLField, ()),
|
||||||
|
(forms.BooleanField, serializers.BooleanField, ()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SerializerDescriptionField(serializers.Field):
|
class SerializerDescriptionField(serializers.Field):
|
||||||
@@ -52,6 +81,13 @@ class ExporterSerializer(serializers.Serializer):
|
|||||||
input_parameters = SerializerDescriptionField(source='_serializer')
|
input_parameters = SerializerDescriptionField(source='_serializer')
|
||||||
|
|
||||||
|
|
||||||
|
class PrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
||||||
|
def to_representation(self, value):
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
return super().to_representation(value)
|
||||||
|
|
||||||
|
|
||||||
class JobRunSerializer(serializers.Serializer):
|
class JobRunSerializer(serializers.Serializer):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
ex = kwargs.pop('exporter')
|
ex = kwargs.pop('exporter')
|
||||||
@@ -66,7 +102,59 @@ class JobRunSerializer(serializers.Serializer):
|
|||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
for k, v in ex.export_form_fields.items():
|
for k, v in ex.export_form_fields.items():
|
||||||
self.fields[k] = form_field_to_serializer_field(v)
|
for m_from, m_to, m_kwargs in simple_mappings:
|
||||||
|
if isinstance(v, m_from):
|
||||||
|
self.fields[k] = m_to(
|
||||||
|
required=v.required,
|
||||||
|
allow_null=not v.required,
|
||||||
|
validators=v.validators,
|
||||||
|
**{kwarg: getattr(v, kwargs, None) for kwarg in m_kwargs}
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
if isinstance(v, forms.NullBooleanField):
|
||||||
|
self.fields[k] = serializers.BooleanField(
|
||||||
|
required=v.required,
|
||||||
|
allow_null=True,
|
||||||
|
validators=v.validators,
|
||||||
|
)
|
||||||
|
if isinstance(v, forms.ModelMultipleChoiceField):
|
||||||
|
self.fields[k] = PrimaryKeyRelatedField(
|
||||||
|
queryset=v.queryset,
|
||||||
|
required=v.required,
|
||||||
|
allow_empty=not v.required,
|
||||||
|
validators=v.validators,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
elif isinstance(v, forms.ModelChoiceField):
|
||||||
|
self.fields[k] = PrimaryKeyRelatedField(
|
||||||
|
queryset=v.queryset,
|
||||||
|
required=v.required,
|
||||||
|
allow_null=not v.required,
|
||||||
|
validators=v.validators,
|
||||||
|
)
|
||||||
|
elif isinstance(v, forms.MultipleChoiceField):
|
||||||
|
self.fields[k] = serializers.MultipleChoiceField(
|
||||||
|
choices=v.choices,
|
||||||
|
required=v.required,
|
||||||
|
allow_empty=not v.required,
|
||||||
|
validators=v.validators,
|
||||||
|
)
|
||||||
|
elif isinstance(v, forms.ChoiceField):
|
||||||
|
self.fields[k] = serializers.ChoiceField(
|
||||||
|
choices=v.choices,
|
||||||
|
required=v.required,
|
||||||
|
allow_null=not v.required,
|
||||||
|
validators=v.validators,
|
||||||
|
)
|
||||||
|
elif isinstance(v, DateFrameField):
|
||||||
|
self.fields[k] = SerializerDateFrameField(
|
||||||
|
required=v.required,
|
||||||
|
allow_null=not v.required,
|
||||||
|
validators=v.validators,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields[k] = FormFieldWrapperField(form_field=v, required=v.required, allow_null=not v.required)
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
if isinstance(data, QueryDict):
|
if isinstance(data, QueryDict):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -109,19 +109,3 @@ class UploadedFileField(serializers.Field):
|
|||||||
return None
|
return None
|
||||||
request = self.context['request']
|
request = self.context['request']
|
||||||
return request.build_absolute_uri(url)
|
return request.build_absolute_uri(url)
|
||||||
|
|
||||||
|
|
||||||
class PluginsField(serializers.Field):
|
|
||||||
|
|
||||||
def to_representation(self, obj):
|
|
||||||
from pretix.base.plugins import get_all_plugins
|
|
||||||
|
|
||||||
return sorted([
|
|
||||||
p.module for p in get_all_plugins()
|
|
||||||
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
|
|
||||||
])
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
return {
|
|
||||||
'plugins': data
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from django import forms
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
|
|
||||||
|
|
||||||
simple_mappings = (
|
|
||||||
(forms.DateField, serializers.DateField, ()),
|
|
||||||
(forms.TimeField, serializers.TimeField, ()),
|
|
||||||
(forms.SplitDateTimeField, serializers.DateTimeField, ()),
|
|
||||||
(forms.DateTimeField, serializers.DateTimeField, ()),
|
|
||||||
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
|
|
||||||
(forms.FloatField, serializers.FloatField, ()),
|
|
||||||
(forms.IntegerField, serializers.IntegerField, ()),
|
|
||||||
(forms.EmailField, serializers.EmailField, ()),
|
|
||||||
(forms.UUIDField, serializers.UUIDField, ()),
|
|
||||||
(forms.URLField, serializers.URLField, ()),
|
|
||||||
(forms.BooleanField, serializers.BooleanField, ()),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
|
||||||
def to_representation(self, value):
|
|
||||||
if isinstance(value, int):
|
|
||||||
return value
|
|
||||||
return super().to_representation(value)
|
|
||||||
|
|
||||||
|
|
||||||
class FormFieldWrapperField(serializers.Field):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.form_field = kwargs.pop('form_field')
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def to_representation(self, value):
|
|
||||||
return self.form_field.widget.format_value(value)
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
d = self.form_field.widget.value_from_datadict({'name': data}, {}, 'name')
|
|
||||||
d = self.form_field.clean(d)
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
def form_field_to_serializer_field(field):
|
|
||||||
for m_from, m_to, m_kwargs in simple_mappings:
|
|
||||||
if isinstance(field, m_from):
|
|
||||||
return m_to(
|
|
||||||
required=field.required,
|
|
||||||
allow_null=not field.required,
|
|
||||||
validators=field.validators,
|
|
||||||
**{kwarg: getattr(field, kwarg, None) for kwarg in m_kwargs}
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(field, forms.NullBooleanField):
|
|
||||||
return serializers.BooleanField(
|
|
||||||
required=field.required,
|
|
||||||
allow_null=True,
|
|
||||||
validators=field.validators,
|
|
||||||
)
|
|
||||||
if isinstance(field, forms.ModelMultipleChoiceField):
|
|
||||||
return PrimaryKeyRelatedField(
|
|
||||||
queryset=field.queryset,
|
|
||||||
required=field.required,
|
|
||||||
allow_empty=not field.required,
|
|
||||||
validators=field.validators,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
elif isinstance(field, forms.ModelChoiceField):
|
|
||||||
return PrimaryKeyRelatedField(
|
|
||||||
queryset=field.queryset,
|
|
||||||
required=field.required,
|
|
||||||
allow_null=not field.required,
|
|
||||||
validators=field.validators,
|
|
||||||
)
|
|
||||||
elif isinstance(field, forms.MultipleChoiceField):
|
|
||||||
return serializers.MultipleChoiceField(
|
|
||||||
choices=field.choices,
|
|
||||||
required=field.required,
|
|
||||||
allow_empty=not field.required,
|
|
||||||
validators=field.validators,
|
|
||||||
)
|
|
||||||
elif isinstance(field, forms.ChoiceField):
|
|
||||||
return serializers.ChoiceField(
|
|
||||||
choices=field.choices,
|
|
||||||
required=field.required,
|
|
||||||
allow_null=not field.required,
|
|
||||||
validators=field.validators,
|
|
||||||
)
|
|
||||||
elif isinstance(field, DateFrameField):
|
|
||||||
return SerializerDateFrameField(
|
|
||||||
required=field.required,
|
|
||||||
allow_null=not field.required,
|
|
||||||
validators=field.validators,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return FormFieldWrapperField(form_field=field, required=field.required, allow_null=not field.required)
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -550,7 +550,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||||
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||||
|
|
||||||
Question.clean_items(event, full_data.get('items') or [])
|
Question.clean_items(event, full_data.get('items'))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate_options(self, value):
|
def validate_options(self, value):
|
||||||
@@ -566,7 +566,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
||||||
items = validated_data.pop('items', [])
|
items = validated_data.pop('items')
|
||||||
|
|
||||||
question = Question.objects.create(**validated_data)
|
question = Question.objects.create(**validated_data)
|
||||||
question.items.set(items)
|
question.items.set(items)
|
||||||
@@ -582,7 +582,7 @@ class QuotaSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Quota
|
model = Quota
|
||||||
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out',
|
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out',
|
||||||
'release_after_exit', 'available', 'available_number', 'ignore_for_event_availability')
|
'release_after_exit', 'available', 'available_number')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -42,7 +42,6 @@ from rest_framework.reverse import reverse
|
|||||||
|
|
||||||
from pretix.api.serializers import CompatibleJSONField
|
from pretix.api.serializers import CompatibleJSONField
|
||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
from pretix.api.serializers.forms import form_field_to_serializer_field
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.item import (
|
from pretix.api.serializers.item import (
|
||||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||||
@@ -50,7 +49,6 @@ from pretix.api.serializers.item import (
|
|||||||
from pretix.api.signals import order_api_details, orderposition_api_details
|
from pretix.api.signals import order_api_details, orderposition_api_details
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.invoicing.transmission import get_transmission_types
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
||||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
||||||
@@ -58,7 +56,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||||
PrintLog, RevokedTicketSecret, Transaction,
|
PrintLog, RevokedTicketSecret,
|
||||||
)
|
)
|
||||||
from pretix.base.pdf import get_images, get_variables
|
from pretix.base.pdf import get_images, get_variables
|
||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
@@ -104,13 +102,6 @@ class CountryField(serializers.Field):
|
|||||||
return str(src) if src else None
|
return str(src) if src else None
|
||||||
|
|
||||||
|
|
||||||
class TransmissionInfoSerializer(serializers.Serializer):
|
|
||||||
def __init__(self, *args, transmission_type, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
for k, v in transmission_type.invoice_address_form_fields.items():
|
|
||||||
self.fields[k] = form_field_to_serializer_field(v)
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||||
country = CompatibleCountryField(source='*')
|
country = CompatibleCountryField(source='*')
|
||||||
name = serializers.CharField(required=False)
|
name = serializers.CharField(required=False)
|
||||||
@@ -118,8 +109,7 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceAddress
|
model = InvoiceAddress
|
||||||
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
||||||
'state', 'vat_id', 'vat_id_validated', 'custom_field', 'internal_reference', 'transmission_type',
|
'state', 'vat_id', 'vat_id_validated', 'custom_field', 'internal_reference')
|
||||||
'transmission_info')
|
|
||||||
read_only_fields = ('last_modified',)
|
read_only_fields = ('last_modified',)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -157,48 +147,6 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
|||||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||||
)
|
)
|
||||||
|
|
||||||
if data.get("transmission_type"):
|
|
||||||
for t in get_transmission_types():
|
|
||||||
if data.get("transmission_type") == t.identifier:
|
|
||||||
if not t.is_available(self.context["request"].event, data.get("country"), data.get("is_business")):
|
|
||||||
raise ValidationError({
|
|
||||||
"transmission_type": "The selected transmission type is not available for this country or address type."
|
|
||||||
})
|
|
||||||
|
|
||||||
ts = TransmissionInfoSerializer(transmission_type=t, data=data.get("transmission_info", {}))
|
|
||||||
try:
|
|
||||||
ts.is_valid(raise_exception=True)
|
|
||||||
except ValidationError as e:
|
|
||||||
raise ValidationError(
|
|
||||||
{"transmission_info": e.detail}
|
|
||||||
)
|
|
||||||
data["transmission_info"] = ts.validated_data
|
|
||||||
|
|
||||||
required_fields = t.invoice_address_form_fields_required(data.get("country"), data.get("is_business"))
|
|
||||||
for r in required_fields:
|
|
||||||
if r in self.fields:
|
|
||||||
if not data.get(r):
|
|
||||||
raise ValidationError(
|
|
||||||
{r: "This field is required for the selected type of invoice transmission."}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if not ts.validated_data.get(r):
|
|
||||||
raise ValidationError(
|
|
||||||
{"transmission_info": {r: "This field is required for the selected type of invoice transmission."}}
|
|
||||||
)
|
|
||||||
break # do not call else branch of for loop
|
|
||||||
elif t.exclusive:
|
|
||||||
if t.is_available(self.context["request"].event, data.get("country"), data.get("is_business")):
|
|
||||||
raise ValidationError({
|
|
||||||
"transmission_type": "The transmission type '%s' must be used for this country or address type." % (
|
|
||||||
t.identifier,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
raise ValidationError(
|
|
||||||
{"transmission_type": "Unknown transmission type."}
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -1005,7 +953,7 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
|
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
|
||||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
|
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
|
||||||
'requested_valid_from', 'use_reusable_medium', 'discount')
|
'requested_valid_from', 'use_reusable_medium')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -1101,10 +1049,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||||
)
|
)
|
||||||
|
|
||||||
if data.get('price') is None and data.get('discount'):
|
|
||||||
raise ValidationError(
|
|
||||||
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
|
|
||||||
)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -1164,7 +1108,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||||
self.fields['positions'].child.fields['discount'].queryset = self.context['event'].discounts.all()
|
|
||||||
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
||||||
self.fields['expires'].required = False
|
self.fields['expires'].required = False
|
||||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||||
@@ -1572,22 +1515,19 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
||||||
|
|
||||||
order_positions = [pos_data['__instance'] for pos_data in positions_data]
|
order_positions = [pos_data['__instance'] for pos_data in positions_data]
|
||||||
if not any([p.get("discount") for p in positions_data]):
|
discount_results = apply_discounts(
|
||||||
# If any discount is set by the client (i.e. pretixPOS), we do not recalculate but believe the client
|
self.context['event'],
|
||||||
# to avoid differences in end results.
|
order.sales_channel,
|
||||||
discount_results = apply_discounts(
|
[
|
||||||
self.context['event'],
|
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
|
||||||
order.sales_channel,
|
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
||||||
[
|
for cp in order_positions
|
||||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
|
]
|
||||||
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
)
|
||||||
for cp in order_positions
|
for cp, (new_price, discount) in zip(order_positions, discount_results):
|
||||||
]
|
if new_price != pos.price and pos._auto_generated_price:
|
||||||
)
|
pos.price = new_price
|
||||||
for cp, (new_price, discount) in zip(order_positions, discount_results):
|
pos.discount = discount
|
||||||
if new_price != pos.price and pos._auto_generated_price:
|
|
||||||
pos.price = new_price
|
|
||||||
pos.discount = discount
|
|
||||||
|
|
||||||
# Save instances
|
# Save instances
|
||||||
for pos_data in positions_data:
|
for pos_data in positions_data:
|
||||||
@@ -1765,14 +1705,12 @@ class LinePositionField(serializers.IntegerField):
|
|||||||
|
|
||||||
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||||
position = LinePositionField(read_only=True)
|
position = LinePositionField(read_only=True)
|
||||||
event_date_from = serializers.DateTimeField(read_only=True, source="period_start")
|
|
||||||
event_date_to = serializers.DateTimeField(read_only=True, source="period_end")
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceLine
|
model = InvoiceLine
|
||||||
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
|
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
|
||||||
'event_date_to', 'period_start', 'period_end', 'gross_value', 'tax_value', 'tax_rate', 'tax_code',
|
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_code', 'tax_name', 'fee_type',
|
||||||
'tax_name', 'fee_type', 'fee_internal_type', 'event_location')
|
'fee_internal_type', 'event_location')
|
||||||
|
|
||||||
|
|
||||||
class InvoiceSerializer(I18nAwareModelSerializer):
|
class InvoiceSerializer(I18nAwareModelSerializer):
|
||||||
@@ -1787,13 +1725,12 @@ class InvoiceSerializer(I18nAwareModelSerializer):
|
|||||||
model = Invoice
|
model = Invoice
|
||||||
fields = ('event', 'order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
|
fields = ('event', 'order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
|
||||||
'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
|
'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
|
||||||
'invoice_to', 'invoice_to_is_business', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street',
|
'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode',
|
||||||
'invoice_to_zipcode', 'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id',
|
'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary',
|
||||||
'invoice_to_beneficiary', 'invoice_to_transmission_info', 'custom_field', 'date', 'refers', 'locale',
|
'custom_field', 'date', 'refers', 'locale',
|
||||||
'introductory_text', 'additional_text', 'payment_provider_text', 'payment_provider_stamp',
|
'introductory_text', 'additional_text', 'payment_provider_text', 'payment_provider_stamp',
|
||||||
'footer_text', 'lines', 'foreign_currency_display', 'foreign_currency_rate',
|
'footer_text', 'lines', 'foreign_currency_display', 'foreign_currency_rate',
|
||||||
'foreign_currency_rate_date', 'internal_reference', 'transmission_type', 'transmission_provider',
|
'foreign_currency_rate_date', 'internal_reference')
|
||||||
'transmission_status', 'transmission_date')
|
|
||||||
|
|
||||||
|
|
||||||
class OrderPaymentCreateSerializer(I18nAwareModelSerializer):
|
class OrderPaymentCreateSerializer(I18nAwareModelSerializer):
|
||||||
@@ -1846,23 +1783,3 @@ class BlockedTicketSecretSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = BlockedTicketSecret
|
model = BlockedTicketSecret
|
||||||
fields = ('id', 'secret', 'updated', 'blocked')
|
fields = ('id', 'secret', 'updated', 'blocked')
|
||||||
|
|
||||||
|
|
||||||
class TransactionSerializer(I18nAwareModelSerializer):
|
|
||||||
order = serializers.SlugRelatedField(slug_field="code", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Transaction
|
|
||||||
fields = (
|
|
||||||
"id", "order", "created", "datetime", "positionid", "count", "item", "variation",
|
|
||||||
"subevent", "price", "tax_rate", "tax_rule", "tax_code", "tax_value", "fee_type",
|
|
||||||
"internal_type"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizerTransactionSerializer(TransactionSerializer):
|
|
||||||
event = serializers.SlugRelatedField(source="order.event", slug_field="slug", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Transaction
|
|
||||||
fields = TransactionSerializer.Meta.fields + ("event",)
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -83,7 +83,6 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
|||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
ocm = self.context['ocm']
|
ocm = self.context['ocm']
|
||||||
check_quotas = self.context.get('check_quotas', True)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ocm.add_position(
|
ocm.add_position(
|
||||||
@@ -97,7 +96,7 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
|||||||
valid_until=validated_data.get('valid_until'),
|
valid_until=validated_data.get('valid_until'),
|
||||||
)
|
)
|
||||||
if self.context.get('commit', True):
|
if self.context.get('commit', True):
|
||||||
ocm.commit(check_quotas=check_quotas)
|
ocm.commit()
|
||||||
return validated_data['order'].positions.order_by('-positionid').first()
|
return validated_data['order'].positions.order_by('-positionid').first()
|
||||||
else:
|
else:
|
||||||
return OrderPosition() # fake to appease DRF
|
return OrderPosition() # fake to appease DRF
|
||||||
@@ -311,7 +310,6 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
ocm = self.context['ocm']
|
ocm = self.context['ocm']
|
||||||
check_quotas = self.context.get('check_quotas', True)
|
|
||||||
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
|
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
|
||||||
item = validated_data.get('item', instance.item)
|
item = validated_data.get('item', instance.item)
|
||||||
variation = validated_data.get('variation', instance.variation)
|
variation = validated_data.get('variation', instance.variation)
|
||||||
@@ -358,7 +356,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
|||||||
ocm.change_ticket_secret(instance, secret)
|
ocm.change_ticket_secret(instance, secret)
|
||||||
|
|
||||||
if self.context.get('commit', True):
|
if self.context.get('commit', True):
|
||||||
ocm.commit(check_quotas=check_quotas)
|
ocm.commit()
|
||||||
instance.refresh_from_db()
|
instance.refresh_from_db()
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
raise ValidationError(str(e))
|
raise ValidationError(str(e))
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -24,7 +24,6 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import transaction
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -33,7 +32,6 @@ from rest_framework.exceptions import ValidationError
|
|||||||
|
|
||||||
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
||||||
from pretix.api.serializers import AsymmetricField
|
from pretix.api.serializers import AsymmetricField
|
||||||
from pretix.api.serializers.fields import PluginsField
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import CompatibleJSONField
|
from pretix.api.serializers.order import CompatibleJSONField
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
@@ -45,10 +43,6 @@ from pretix.base.models import (
|
|||||||
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||||
from pretix.base.plugins import (
|
|
||||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
|
||||||
PLUGIN_LEVEL_ORGANIZER,
|
|
||||||
)
|
|
||||||
from pretix.base.services.mail import SendMailException, mail
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
from pretix.base.settings import validate_organizer_settings
|
from pretix.base.settings import validate_organizer_settings
|
||||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||||
@@ -59,47 +53,13 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||||
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
|
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
|
||||||
plugins = PluginsField(required=False, source='*')
|
|
||||||
name = serializers.CharField(read_only=True)
|
|
||||||
slug = serializers.CharField(read_only=True)
|
|
||||||
|
|
||||||
def get_organizer_url(self, organizer):
|
def get_organizer_url(self, organizer):
|
||||||
return build_absolute_uri(organizer, 'presale:organizer.index')
|
return build_absolute_uri(organizer, 'presale:organizer.index')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Organizer
|
model = Organizer
|
||||||
fields = ('name', 'slug', 'public_url', 'plugins')
|
fields = ('name', 'slug', 'public_url')
|
||||||
|
|
||||||
def validate_plugins(self, value):
|
|
||||||
from pretix.base.plugins import get_all_plugins
|
|
||||||
|
|
||||||
plugins_available = {
|
|
||||||
p.module: p for p in get_all_plugins(organizer=self.instance)
|
|
||||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
|
||||||
}
|
|
||||||
settings_holder = self.instance
|
|
||||||
|
|
||||||
allowed_levels = (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID)
|
|
||||||
for plugin in value.get('plugins'):
|
|
||||||
if plugin not in plugins_available:
|
|
||||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
|
||||||
if getattr(plugins_available[plugin], 'restricted', False):
|
|
||||||
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
|
||||||
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
|
||||||
if getattr(plugins_available[plugin], 'level', PLUGIN_LEVEL_EVENT) not in allowed_levels:
|
|
||||||
raise ValidationError('Plugin cannot be enabled on this level: \'{name}\'.'.format(name=plugin))
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
plugins = validated_data.pop('plugins', None)
|
|
||||||
organizer = super().update(instance, validated_data)
|
|
||||||
# Plugins
|
|
||||||
if plugins is not None:
|
|
||||||
organizer.set_active_plugins(plugins)
|
|
||||||
organizer.save()
|
|
||||||
return organizer
|
|
||||||
|
|
||||||
|
|
||||||
class SeatingPlanSerializer(I18nAwareModelSerializer):
|
class SeatingPlanSerializer(I18nAwareModelSerializer):
|
||||||
@@ -484,7 +444,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
|||||||
'reusable_media_type_nfc_mf0aes',
|
'reusable_media_type_nfc_mf0aes',
|
||||||
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
|
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
|
||||||
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
|
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
|
||||||
'reusable_media_type_nfc_mf0aes_random_uid',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,8 +19,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
@@ -66,15 +64,14 @@ class SeatGuidField(serializers.CharField):
|
|||||||
|
|
||||||
class VoucherSerializer(I18nAwareModelSerializer):
|
class VoucherSerializer(I18nAwareModelSerializer):
|
||||||
seat = SeatGuidField(allow_null=True, required=False)
|
seat = SeatGuidField(allow_null=True, required=False)
|
||||||
budget_used = serializers.DecimalField(read_only=True, max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Voucher
|
model = Voucher
|
||||||
fields = ('id', 'created', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
||||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||||
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
|
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
|
||||||
'all_bundles_included', 'budget', 'budget_used')
|
'all_bundles_included')
|
||||||
read_only_fields = ('id', 'redeemed', 'budget_used')
|
read_only_fields = ('id', 'redeemed')
|
||||||
list_serializer_class = VoucherListSerializer
|
list_serializer_class = VoucherListSerializer
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -21,22 +21,22 @@
|
|||||||
#
|
#
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import Signal, receiver
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.api.models import ApiCall, WebHookCall
|
from pretix.api.models import ApiCall, WebHookCall
|
||||||
from pretix.base.signals import EventPluginSignal, GlobalSignal, periodic_task
|
from pretix.base.signals import EventPluginSignal, periodic_task
|
||||||
from pretix.helpers.periodic import minimum_interval
|
from pretix.helpers.periodic import minimum_interval
|
||||||
|
|
||||||
register_webhook_events = GlobalSignal()
|
register_webhook_events = Signal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known webhook events. Receivers should return an
|
This signal is sent out to get all known webhook events. Receivers should return an
|
||||||
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
|
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
|
||||||
instances.
|
instances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
register_device_security_profile = GlobalSignal()
|
register_device_security_profile = Signal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known device security_profiles. Receivers should
|
This signal is sent out to get all known device security_profiles. Receivers should
|
||||||
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
|
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -66,7 +66,6 @@ orga_router.register(r'orders', order.OrganizerOrderViewSet)
|
|||||||
orga_router.register(r'invoices', order.InvoiceViewSet)
|
orga_router.register(r'invoices', order.InvoiceViewSet)
|
||||||
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
|
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
|
||||||
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
|
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
|
||||||
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
|
|
||||||
|
|
||||||
team_router = routers.DefaultRouter()
|
team_router = routers.DefaultRouter()
|
||||||
team_router.register(r'members', organizer.TeamMemberViewSet)
|
team_router.register(r'members', organizer.TeamMemberViewSet)
|
||||||
@@ -84,7 +83,6 @@ event_router.register(r'quotas', item.QuotaViewSet)
|
|||||||
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||||
event_router.register(r'orders', order.EventOrderViewSet)
|
event_router.register(r'orders', order.EventOrderViewSet)
|
||||||
event_router.register(r'orderpositions', order.OrderPositionViewSet)
|
event_router.register(r'orderpositions', order.OrderPositionViewSet)
|
||||||
event_router.register(r'transactions', order.TransactionViewSet)
|
|
||||||
event_router.register(r'invoices', order.InvoiceViewSet)
|
event_router.register(r'invoices', order.InvoiceViewSet)
|
||||||
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
||||||
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
||||||
@@ -132,8 +130,6 @@ urlpatterns = [
|
|||||||
name="checkinrpc.redeem"),
|
name="checkinrpc.redeem"),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/search/$', checkin.CheckinRPCSearchView.as_view(),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/search/$', checkin.CheckinRPCSearchView.as_view(),
|
||||||
name="checkinrpc.search"),
|
name="checkinrpc.search"),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/annul/$', checkin.CheckinRPCAnnulView.as_view(),
|
|
||||||
name="checkinrpc.annul"),
|
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/settings/$', organizer.OrganizerSettingsView.as_view(),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/settings/$', organizer.OrganizerSettingsView.as_view(),
|
||||||
name="organizer.settings"),
|
name="organizer.settings"),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/giftcards/(?P<giftcard>[^/]+)/', include(giftcard_router.urls)),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/giftcards/(?P<giftcard>[^/]+)/', include(giftcard_router.urls)),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -20,13 +20,12 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import operator
|
import operator
|
||||||
from datetime import timedelta
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError as BaseValidationError
|
from django.core.exceptions import ValidationError as BaseValidationError
|
||||||
from django.db import connection, transaction
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
|
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
|
||||||
prefetch_related_objects,
|
prefetch_related_objects,
|
||||||
@@ -40,19 +39,17 @@ from django.utils.translation import gettext
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from packaging.version import parse
|
from packaging.version import parse
|
||||||
from rest_framework import status, views, viewsets
|
from rest_framework import views, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import (
|
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||||
NotFound, PermissionDenied, ValidationError,
|
|
||||||
)
|
|
||||||
from rest_framework.fields import DateTimeField
|
from rest_framework.fields import DateTimeField
|
||||||
from rest_framework.generics import ListAPIView
|
from rest_framework.generics import ListAPIView
|
||||||
from rest_framework.permissions import SAFE_METHODS
|
from rest_framework.permissions import SAFE_METHODS
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.serializers.checkin import (
|
from pretix.api.serializers.checkin import (
|
||||||
CheckinListSerializer, CheckinRPCAnnulInputSerializer,
|
CheckinListSerializer, CheckinRPCRedeemInputSerializer,
|
||||||
CheckinRPCRedeemInputSerializer, MiniCheckinListSerializer,
|
MiniCheckinListSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.item import QuestionSerializer
|
from pretix.api.serializers.item import QuestionSerializer
|
||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
@@ -69,8 +66,6 @@ from pretix.base.models.orders import PrintLog
|
|||||||
from pretix.base.services.checkin import (
|
from pretix.base.services.checkin import (
|
||||||
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import checkin_annulled
|
|
||||||
from pretix.helpers import OF_SELF
|
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class CheckinListFilter(FilterSet):
|
class CheckinListFilter(FilterSet):
|
||||||
@@ -818,7 +813,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
ctx['expand'] = self.request.query_params.getlist('expand')
|
ctx['expand'] = self.request.query_params.getlist('expand')
|
||||||
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
|
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_filterset_kwargs(self):
|
def get_filterset_kwargs(self):
|
||||||
@@ -837,9 +832,9 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
def get_queryset(self, ignore_status=False, ignore_products=False):
|
def get_queryset(self, ignore_status=False, ignore_products=False):
|
||||||
qs = _checkin_list_position_queryset(
|
qs = _checkin_list_position_queryset(
|
||||||
[self.checkinlist],
|
[self.checkinlist],
|
||||||
ignore_status=self.request.query_params.get('ignore_status', 'false').lower() == 'true' or ignore_status,
|
ignore_status=self.request.query_params.get('ignore_status', 'false') == 'true' or ignore_status,
|
||||||
ignore_products=ignore_products,
|
ignore_products=ignore_products,
|
||||||
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
||||||
expand=self.request.query_params.getlist('expand'),
|
expand=self.request.query_params.getlist('expand'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -881,7 +876,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
expand=self.request.query_params.getlist('expand'),
|
expand=self.request.query_params.getlist('expand'),
|
||||||
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
||||||
questions_supported=self.request.data.get('questions_supported', True),
|
questions_supported=self.request.data.get('questions_supported', True),
|
||||||
canceled_supported=self.request.data.get('canceled_supported', False),
|
canceled_supported=self.request.data.get('canceled_supported', False),
|
||||||
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||||
@@ -916,7 +911,7 @@ class CheckinRPCRedeemView(views.APIView):
|
|||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
expand=self.request.query_params.getlist('expand'),
|
expand=self.request.query_params.getlist('expand'),
|
||||||
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
||||||
questions_supported=s.validated_data['questions_supported'],
|
questions_supported=s.validated_data['questions_supported'],
|
||||||
use_order_locale=s.validated_data['use_order_locale'],
|
use_order_locale=s.validated_data['use_order_locale'],
|
||||||
canceled_supported=True,
|
canceled_supported=True,
|
||||||
@@ -994,9 +989,9 @@ class CheckinRPCSearchView(ListAPIView):
|
|||||||
def get_queryset(self, ignore_status=False, ignore_products=False):
|
def get_queryset(self, ignore_status=False, ignore_products=False):
|
||||||
qs = _checkin_list_position_queryset(
|
qs = _checkin_list_position_queryset(
|
||||||
self.lists,
|
self.lists,
|
||||||
ignore_status=self.request.query_params.get('ignore_status', 'false').lower() == 'true' or ignore_status,
|
ignore_status=self.request.query_params.get('ignore_status', 'false') == 'true' or ignore_status,
|
||||||
ignore_products=ignore_products,
|
ignore_products=ignore_products,
|
||||||
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
||||||
expand=self.request.query_params.getlist('expand'),
|
expand=self.request.query_params.getlist('expand'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1004,79 +999,3 @@ class CheckinRPCSearchView(ListAPIView):
|
|||||||
qs = qs.none()
|
qs = qs.none()
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class CheckinRPCAnnulView(views.APIView):
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
|
||||||
events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
|
|
||||||
elif self.request.user.is_authenticated:
|
|
||||||
events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
|
|
||||||
organizer=self.request.organizer
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError("unknown authentication method")
|
|
||||||
|
|
||||||
s = CheckinRPCAnnulInputSerializer(data=request.data, context={'events': events})
|
|
||||||
s.is_valid(raise_exception=True)
|
|
||||||
|
|
||||||
with transaction.atomic():
|
|
||||||
try:
|
|
||||||
qs = Checkin.all.all()
|
|
||||||
if isinstance(request.auth, Device):
|
|
||||||
qs = qs.filter(device=request.auth)
|
|
||||||
ci = qs.select_for_update(
|
|
||||||
of=OF_SELF,
|
|
||||||
).select_related("position", "position__order", "position__order__event").get(
|
|
||||||
list__in=s.validated_data['lists'],
|
|
||||||
nonce=s.validated_data['nonce'],
|
|
||||||
)
|
|
||||||
if connection.features.has_select_for_update_of and ci.position_id:
|
|
||||||
# Lock position as well, can't do it with of= above because relation is nullable
|
|
||||||
OrderPosition.objects.select_for_update(of=OF_SELF).get(pk=ci.position_id)
|
|
||||||
|
|
||||||
if not ci.successful or not ci.position:
|
|
||||||
raise ValidationError("Cannot annul an unsuccessful checkin")
|
|
||||||
except Checkin.DoesNotExist:
|
|
||||||
raise NotFound("No check-in found based on nonce")
|
|
||||||
except Checkin.MultipleObjectsReturned:
|
|
||||||
raise ValidationError("Multiple check-ins found based on nonce")
|
|
||||||
|
|
||||||
annulment_time = s.validated_data.get("datetime") or now()
|
|
||||||
|
|
||||||
if annulment_time - ci.datetime > timedelta(minutes=15):
|
|
||||||
# Compare to sent datetime, which makes this cheatable, but allows offline annulment of checkins
|
|
||||||
ci.position.order.log_action('pretix.event.checkin.annulment.ignored', data={
|
|
||||||
'checkin': ci.pk,
|
|
||||||
'position': ci.position.id,
|
|
||||||
'positionid': ci.position.positionid,
|
|
||||||
'datetime': annulment_time,
|
|
||||||
'error_explanation': s.validated_data.get("error_explanation"),
|
|
||||||
'type': ci.type,
|
|
||||||
'list': ci.list_id,
|
|
||||||
}, user=request.user, auth=request.auth)
|
|
||||||
return Response({
|
|
||||||
"non_field_errors": ["Annulment is not allowed more than 15 minutes after check-in"]
|
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
if ci.device and ci.device != request.auth:
|
|
||||||
return Response({
|
|
||||||
"non_field_errors": ["Annulment is only allowed from the same device"]
|
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
ci.successful = False
|
|
||||||
ci.error_reason = Checkin.REASON_ANNULLED
|
|
||||||
ci.error_explanation = s.validated_data.get("error_explanation")
|
|
||||||
ci.save(update_fields=["successful", "error_reason", "error_explanation"])
|
|
||||||
ci.position.order.log_action('pretix.event.checkin.annulled', data={
|
|
||||||
'checkin': ci.pk,
|
|
||||||
'position': ci.position.id,
|
|
||||||
'positionid': ci.position.positionid,
|
|
||||||
'datetime': annulment_time,
|
|
||||||
'error_explanation': s.validated_data.get("error_explanation"),
|
|
||||||
'type': ci.type,
|
|
||||||
'list': ci.list_id,
|
|
||||||
}, user=request.user, auth=request.auth)
|
|
||||||
checkin_annulled.send(ci.position.order.event, checkin=ci)
|
|
||||||
|
|
||||||
return Response({"status": "ok"}, status=status.HTTP_200_OK)
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -57,9 +57,9 @@ from pretix.api.serializers.order import (
|
|||||||
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
|
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
|
||||||
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
||||||
OrderPositionSerializer, OrderRefundCreateSerializer,
|
OrderPositionSerializer, OrderRefundCreateSerializer,
|
||||||
OrderRefundSerializer, OrderSerializer, OrganizerTransactionSerializer,
|
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
||||||
PriceCalcSerializer, PrintLogSerializer, RevokedTicketSecretSerializer,
|
PrintLogSerializer, RevokedTicketSecretSerializer,
|
||||||
SimulatedOrderSerializer, TransactionSerializer,
|
SimulatedOrderSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.orderchange import (
|
from pretix.api.serializers.orderchange import (
|
||||||
BlockNameSerializer, OrderChangeOperationSerializer,
|
BlockNameSerializer, OrderChangeOperationSerializer,
|
||||||
@@ -80,7 +80,6 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret,
|
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret,
|
||||||
Transaction,
|
|
||||||
)
|
)
|
||||||
from pretix.base.payment import PaymentException
|
from pretix.base.payment import PaymentException
|
||||||
from pretix.base.pdf import get_images
|
from pretix.base.pdf import get_images
|
||||||
@@ -88,7 +87,7 @@ from pretix.base.secrets import assign_ticket_secret
|
|||||||
from pretix.base.services import tickets
|
from pretix.base.services import tickets
|
||||||
from pretix.base.services.invoices import (
|
from pretix.base.services.invoices import (
|
||||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
|
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
|
||||||
regenerate_invoice, transmit_invoice,
|
regenerate_invoice,
|
||||||
)
|
)
|
||||||
from pretix.base.services.mail import SendMailException
|
from pretix.base.services.mail import SendMailException
|
||||||
from pretix.base.services.orders import (
|
from pretix.base.services.orders import (
|
||||||
@@ -228,7 +227,7 @@ class OrderViewSetMixin:
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.get_base_queryset()
|
qs = self.get_base_queryset()
|
||||||
if 'fees' not in self.request.GET.getlist('exclude'):
|
if 'fees' not in self.request.GET.getlist('exclude'):
|
||||||
if self.request.query_params.get('include_canceled_fees', 'false').lower() == 'true':
|
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
|
||||||
fqs = OrderFee.all
|
fqs = OrderFee.all
|
||||||
else:
|
else:
|
||||||
fqs = OrderFee.objects
|
fqs = OrderFee.objects
|
||||||
@@ -246,11 +245,11 @@ class OrderViewSetMixin:
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
def _positions_prefetch(self, request):
|
def _positions_prefetch(self, request):
|
||||||
if request.query_params.get('include_canceled_positions', 'false').lower() == 'true':
|
if request.query_params.get('include_canceled_positions', 'false') == 'true':
|
||||||
opq = OrderPosition.all
|
opq = OrderPosition.all
|
||||||
else:
|
else:
|
||||||
opq = OrderPosition.objects
|
opq = OrderPosition.objects
|
||||||
if request.query_params.get('pdf_data', 'false').lower() == 'true' and getattr(request, 'event', None):
|
if request.query_params.get('pdf_data', 'false') == 'true' and getattr(request, 'event', None):
|
||||||
prefetch_related_objects([request.organizer], 'meta_properties')
|
prefetch_related_objects([request.organizer], 'meta_properties')
|
||||||
prefetch_related_objects(
|
prefetch_related_objects(
|
||||||
[request.event],
|
[request.event],
|
||||||
@@ -344,7 +343,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
|
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_base_queryset(self):
|
def get_base_queryset(self):
|
||||||
@@ -743,7 +742,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
user=request.user if request.user.is_authenticated else None,
|
user=request.user if request.user.is_authenticated else None,
|
||||||
auth=request.auth,
|
auth=request.auth,
|
||||||
)
|
)
|
||||||
order_placed.send(self.request.event, order=order, bulk=False)
|
order_placed.send(self.request.event, order=order)
|
||||||
if order.status == Order.STATUS_PAID:
|
if order.status == Order.STATUS_PAID:
|
||||||
order_paid.send(self.request.event, order=order)
|
order_paid.send(self.request.event, order=order)
|
||||||
order.log_action(
|
order.log_action(
|
||||||
@@ -764,13 +763,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
) and not order.invoices.last()
|
) and not order.invoices.last()
|
||||||
invoice = None
|
invoice = None
|
||||||
if gen_invoice:
|
if gen_invoice:
|
||||||
try:
|
invoice = generate_invoice(order, trigger_pdf=True)
|
||||||
invoice = generate_invoice(order, trigger_pdf=True)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception("Could not generate invoice.")
|
|
||||||
order.log_action("pretix.event.order.invoice.failed", data={
|
|
||||||
"exception": str(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
# Refresh serializer only after running signals
|
# Refresh serializer only after running signals
|
||||||
prefetch_related_objects([order], self._positions_prefetch(request))
|
prefetch_related_objects([order], self._positions_prefetch(request))
|
||||||
@@ -949,7 +942,6 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def change(self, request, **kwargs):
|
def change(self, request, **kwargs):
|
||||||
order = self.get_object()
|
order = self.get_object()
|
||||||
check_quotas = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
|
|
||||||
|
|
||||||
serializer = OrderChangeOperationSerializer(
|
serializer = OrderChangeOperationSerializer(
|
||||||
context={'order': order, **self.get_serializer_context()},
|
context={'order': order, **self.get_serializer_context()},
|
||||||
@@ -1015,7 +1007,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
elif serializer.validated_data.get('recalculate_taxes') == 'keep_gross':
|
elif serializer.validated_data.get('recalculate_taxes') == 'keep_gross':
|
||||||
ocm.recalculate_taxes(keep='gross')
|
ocm.recalculate_taxes(keep='gross')
|
||||||
|
|
||||||
ocm.commit(check_quotas=check_quotas)
|
ocm.commit()
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
raise ValidationError(str(e))
|
raise ValidationError(str(e))
|
||||||
|
|
||||||
@@ -1093,18 +1085,17 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
|
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
|
||||||
ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.query_params.get('include_canceled_positions', 'false').lower() == 'true':
|
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
|
||||||
qs = OrderPosition.all
|
qs = OrderPosition.all
|
||||||
else:
|
else:
|
||||||
qs = OrderPosition.objects
|
qs = OrderPosition.objects
|
||||||
|
|
||||||
qs = qs.filter(order__event=self.request.event)
|
qs = qs.filter(order__event=self.request.event)
|
||||||
if self.request.query_params.get('pdf_data', 'false').lower() == 'true':
|
if self.request.query_params.get('pdf_data', 'false') == 'true':
|
||||||
prefetch_related_objects([self.request.organizer], 'meta_properties')
|
prefetch_related_objects([self.request.organizer], 'meta_properties')
|
||||||
prefetch_related_objects(
|
prefetch_related_objects(
|
||||||
[self.request.event],
|
[self.request.event],
|
||||||
@@ -1669,9 +1660,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
else:
|
else:
|
||||||
mark_refunded = request.data.get('mark_canceled', False)
|
mark_refunded = request.data.get('mark_canceled', False)
|
||||||
|
|
||||||
if not isinstance(request.data.get("comment", ""), str):
|
|
||||||
return Response({'comment': 'Invalid type.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
if payment.state != OrderPayment.PAYMENT_STATE_CONFIRMED:
|
if payment.state != OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||||
return Response({'detail': 'Invalid state of payment.'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'detail': 'Invalid state of payment.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@@ -1698,7 +1686,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
amount=amount,
|
amount=amount,
|
||||||
provider=payment.provider,
|
provider=payment.provider,
|
||||||
info='{}',
|
info='{}',
|
||||||
comment=request.data.get("comment"),
|
|
||||||
)
|
)
|
||||||
payment.order.log_action('pretix.event.order.refund.created', {
|
payment.order.log_action('pretix.event.order.refund.created', {
|
||||||
'local_id': r.local_id,
|
'local_id': r.local_id,
|
||||||
@@ -1901,12 +1888,6 @@ class RetryException(APIException):
|
|||||||
default_code = 'retry_later'
|
default_code = 'retry_later'
|
||||||
|
|
||||||
|
|
||||||
class CurrentlyInflightException(APIException):
|
|
||||||
status_code = 409
|
|
||||||
default_detail = 'The requested action is already in progress.'
|
|
||||||
default_code = 'currently_inflight'
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = InvoiceSerializer
|
serializer_class = InvoiceSerializer
|
||||||
queryset = Invoice.objects.none()
|
queryset = Invoice.objects.none()
|
||||||
@@ -1955,52 +1936,13 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
|
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
|
||||||
def transmit(self, request, **kwargs):
|
|
||||||
invoice = self.get_object()
|
|
||||||
if invoice.shredded:
|
|
||||||
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
|
||||||
|
|
||||||
if invoice.transmission_status != Invoice.TRANSMISSION_STATUS_PENDING:
|
|
||||||
raise PermissionDenied('The invoice is not in pending state.')
|
|
||||||
|
|
||||||
transmit_invoice.apply_async(args=(self.request.event.pk, invoice.pk, False))
|
|
||||||
return Response(status=204)
|
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
|
||||||
def retransmit(self, request, **kwargs):
|
|
||||||
invoice = self.get_object()
|
|
||||||
if invoice.shredded:
|
|
||||||
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
|
||||||
|
|
||||||
with transaction.atomic(durable=True):
|
|
||||||
invoice = Invoice.objects.select_for_update(of=OF_SELF).get(pk=invoice.pk)
|
|
||||||
|
|
||||||
if invoice.transmission_status == Invoice.TRANSMISSION_STATUS_INFLIGHT:
|
|
||||||
raise CurrentlyInflightException()
|
|
||||||
|
|
||||||
invoice.transmission_status = Invoice.TRANSMISSION_STATUS_PENDING
|
|
||||||
invoice.transmission_date = now()
|
|
||||||
invoice.save(update_fields=["transmission_status", "transmission_date"])
|
|
||||||
invoice.order.log_action(
|
|
||||||
'pretix.event.order.invoice.retransmitted',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data={
|
|
||||||
'invoice': invoice.pk,
|
|
||||||
'full_invoice_no': invoice.full_invoice_no,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
transmit_invoice.apply_async(args=(self.request.event.pk, invoice.pk, True))
|
|
||||||
return Response(status=204)
|
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def regenerate(self, request, **kwargs):
|
def regenerate(self, request, **kwargs):
|
||||||
inv = self.get_object()
|
inv = self.get_object()
|
||||||
if inv.canceled:
|
if inv.canceled:
|
||||||
raise ValidationError('The invoice has already been canceled.')
|
raise ValidationError('The invoice has already been canceled.')
|
||||||
if not inv.regenerate_allowed:
|
if not inv.event.settings.invoice_regenerate_allowed:
|
||||||
raise PermissionDenied('Invoice may not be regenerated.')
|
raise PermissionDenied('Invoices may not be changed after they are created.')
|
||||||
elif inv.shredded:
|
elif inv.shredded:
|
||||||
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||||
elif inv.sent_to_organizer:
|
elif inv.sent_to_organizer:
|
||||||
@@ -2088,61 +2030,3 @@ class BlockedSecretViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return BlockedTicketSecret.objects.filter(event=self.request.event)
|
return BlockedTicketSecret.objects.filter(event=self.request.event)
|
||||||
|
|
||||||
|
|
||||||
with scopes_disabled():
|
|
||||||
class TransactionFilter(FilterSet):
|
|
||||||
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
|
||||||
event = django_filters.CharFilter(field_name='order__event', lookup_expr='slug__iexact')
|
|
||||||
datetime_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
|
||||||
datetime_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt')
|
|
||||||
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
|
|
||||||
created_before = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='lt')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Transaction
|
|
||||||
fields = {
|
|
||||||
'item': ['exact', 'in'],
|
|
||||||
'variation': ['exact', 'in'],
|
|
||||||
'subevent': ['exact', 'in'],
|
|
||||||
'tax_rule': ['exact', 'in'],
|
|
||||||
'tax_code': ['exact', 'in'],
|
|
||||||
'tax_rate': ['exact', 'in'],
|
|
||||||
'fee_type': ['exact', 'in'],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionViewSet(viewsets.ReadOnlyModelViewSet):
|
|
||||||
serializer_class = TransactionSerializer
|
|
||||||
queryset = Transaction.objects.none()
|
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
|
||||||
ordering = ('datetime', 'pk')
|
|
||||||
ordering_fields = ('datetime', 'created', 'id',)
|
|
||||||
filterset_class = TransactionFilter
|
|
||||||
permission = 'can_view_orders'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return Transaction.objects.filter(order__event=self.request.event).select_related("order")
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizerTransactionViewSet(TransactionViewSet):
|
|
||||||
serializer_class = OrganizerTransactionSerializer
|
|
||||||
permission = None
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
qs = Transaction.objects.filter(
|
|
||||||
order__event__organizer=self.request.organizer
|
|
||||||
).select_related("order", "order__event")
|
|
||||||
|
|
||||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
|
||||||
qs = qs.filter(
|
|
||||||
order__event__in=self.request.auth.get_events_with_permission("can_view_orders"),
|
|
||||||
)
|
|
||||||
elif self.request.user.is_authenticated:
|
|
||||||
qs = qs.filter(
|
|
||||||
order__event__in=self.request.user.get_events_with_permission("can_view_orders", request=self.request)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise PermissionDenied("Unknown authentication scheme")
|
|
||||||
|
|
||||||
return qs
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,9 +19,7 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import operator
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
@@ -50,18 +48,15 @@ from pretix.api.serializers.organizer import (
|
|||||||
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Customer, Device, Event, GiftCard, GiftCardTransaction, LogEntry,
|
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||||
Membership, MembershipType, Organizer, SalesChannel, SeatingPlan, Team,
|
MembershipType, Organizer, SalesChannel, SeatingPlan, Team, TeamAPIToken,
|
||||||
TeamAPIToken, TeamInvite, User,
|
TeamInvite, User,
|
||||||
)
|
|
||||||
from pretix.base.plugins import (
|
|
||||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
|
||||||
)
|
)
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
|
|
||||||
|
|
||||||
class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
serializer_class = OrganizerSerializer
|
serializer_class = OrganizerSerializer
|
||||||
queryset = Organizer.objects.none()
|
queryset = Organizer.objects.none()
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
@@ -70,7 +65,6 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
filter_backends = (TotalOrderingFilter,)
|
filter_backends = (TotalOrderingFilter,)
|
||||||
ordering = ('slug',)
|
ordering = ('slug',)
|
||||||
ordering_fields = ('name', 'slug')
|
ordering_fields = ('name', 'slug')
|
||||||
write_permission = "can_change_organizer_settings"
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
@@ -89,67 +83,6 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
|||||||
else:
|
else:
|
||||||
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
|
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
|
||||||
|
|
||||||
@transaction.atomic()
|
|
||||||
def perform_update(self, serializer):
|
|
||||||
from pretix.base.plugins import get_all_plugins
|
|
||||||
|
|
||||||
original_data = self.get_serializer(instance=serializer.instance).data
|
|
||||||
|
|
||||||
current_plugins_value = serializer.instance.get_plugins()
|
|
||||||
updated_plugins_value = serializer.validated_data.get('plugins', None)
|
|
||||||
|
|
||||||
super().perform_update(serializer)
|
|
||||||
|
|
||||||
if serializer.data == original_data:
|
|
||||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
|
||||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
|
||||||
return
|
|
||||||
|
|
||||||
if updated_plugins_value is not None and set(updated_plugins_value) != set(current_plugins_value):
|
|
||||||
enabled = {m: 'enabled' for m in updated_plugins_value if m not in current_plugins_value}
|
|
||||||
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
|
|
||||||
changed = merge_dicts(enabled, disabled)
|
|
||||||
|
|
||||||
plugins_available = {
|
|
||||||
p.module: p
|
|
||||||
for p in get_all_plugins(organizer=serializer.instance)
|
|
||||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
|
||||||
}
|
|
||||||
qs = []
|
|
||||||
for module in disabled:
|
|
||||||
pluginmeta = plugins_available[module]
|
|
||||||
level = getattr(pluginmeta, 'level', PLUGIN_LEVEL_EVENT)
|
|
||||||
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
|
|
||||||
qs.append(Q(plugins__regex='(^|,)' + module + '(,|$)'))
|
|
||||||
|
|
||||||
if qs:
|
|
||||||
events_to_disable = set(self.request.organizer.events.filter(
|
|
||||||
reduce(operator.or_, qs)
|
|
||||||
).values_list("pk", flat=True))
|
|
||||||
logentries_to_save = []
|
|
||||||
events_to_save = []
|
|
||||||
|
|
||||||
for e in self.request.organizer.events.filter(pk__in=events_to_disable):
|
|
||||||
for module in disabled:
|
|
||||||
if module in e.get_plugins():
|
|
||||||
logentries_to_save.append(
|
|
||||||
e.log_action('pretix.event.plugins.disabled', user=self.request.user, auth=self.request.auth,
|
|
||||||
data={'plugin': module}, save=False)
|
|
||||||
)
|
|
||||||
e.disable_plugin(module)
|
|
||||||
events_to_save.append(e)
|
|
||||||
|
|
||||||
Event.objects.bulk_update(events_to_save, fields=["plugins"])
|
|
||||||
LogEntry.objects.bulk_create(logentries_to_save)
|
|
||||||
|
|
||||||
for module, operation in changed.items():
|
|
||||||
serializer.instance.log_action(
|
|
||||||
'pretix.organizer.plugins.' + operation,
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data={'plugin': module}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SeatingPlanViewSet(viewsets.ModelViewSet):
|
class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = SeatingPlanSerializer
|
serializer_class = SeatingPlanSerializer
|
||||||
@@ -546,8 +479,7 @@ class DeviceViewSet(mixins.CreateModelMixin,
|
|||||||
|
|
||||||
|
|
||||||
class OrganizerSettingsView(views.APIView):
|
class OrganizerSettingsView(views.APIView):
|
||||||
permission = None
|
permission = 'can_change_organizer_settings'
|
||||||
write_permission = 'can_change_organizer_settings'
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -78,13 +78,6 @@ class WebhookEvent:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError() # NOQA
|
raise NotImplementedError() # NOQA
|
||||||
|
|
||||||
@property
|
|
||||||
def help_text(self) -> str:
|
|
||||||
"""
|
|
||||||
A human-readable description
|
|
||||||
"""
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_webhook_events():
|
def get_all_webhook_events():
|
||||||
global _ALL_EVENTS
|
global _ALL_EVENTS
|
||||||
@@ -104,10 +97,9 @@ def get_all_webhook_events():
|
|||||||
|
|
||||||
|
|
||||||
class ParametrizedWebhookEvent(WebhookEvent):
|
class ParametrizedWebhookEvent(WebhookEvent):
|
||||||
def __init__(self, action_type, verbose_name, help_text=""):
|
def __init__(self, action_type, verbose_name):
|
||||||
self._action_type = action_type
|
self._action_type = action_type
|
||||||
self._verbose_name = verbose_name
|
self._verbose_name = verbose_name
|
||||||
self._help_text = help_text
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -118,10 +110,6 @@ class ParametrizedWebhookEvent(WebhookEvent):
|
|||||||
def verbose_name(self):
|
def verbose_name(self):
|
||||||
return self._verbose_name
|
return self._verbose_name
|
||||||
|
|
||||||
@property
|
|
||||||
def help_text(self):
|
|
||||||
return self._help_text
|
|
||||||
|
|
||||||
|
|
||||||
class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
|
class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
|
||||||
def build_payload(self, logentry: LogEntry):
|
def build_payload(self, logentry: LogEntry):
|
||||||
@@ -173,19 +161,6 @@ class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ParametrizedVoucherWebhookEvent(ParametrizedWebhookEvent):
|
|
||||||
|
|
||||||
def build_payload(self, logentry: LogEntry):
|
|
||||||
# do not use content_object, this is also called in deletion
|
|
||||||
return {
|
|
||||||
'notification_id': logentry.pk,
|
|
||||||
'organizer': logentry.event.organizer.slug,
|
|
||||||
'event': logentry.event.slug,
|
|
||||||
'voucher': logentry.object_id,
|
|
||||||
'action': logentry.action_type,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ParametrizedSubEventWebhookEvent(ParametrizedWebhookEvent):
|
class ParametrizedSubEventWebhookEvent(ParametrizedWebhookEvent):
|
||||||
|
|
||||||
def build_payload(self, logentry: LogEntry):
|
def build_payload(self, logentry: LogEntry):
|
||||||
@@ -371,9 +346,8 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
),
|
),
|
||||||
ParametrizedItemWebhookEvent(
|
ParametrizedItemWebhookEvent(
|
||||||
'pretix.event.item.*',
|
'pretix.event.item.*',
|
||||||
_('Product changed'),
|
_('Product changed (including product added or deleted and including changes to nested objects like '
|
||||||
_('This includes product added or deleted and changes to nested objects like '
|
'variations or bundles)'),
|
||||||
'variations or bundles.'),
|
|
||||||
),
|
),
|
||||||
ParametrizedEventWebhookEvent(
|
ParametrizedEventWebhookEvent(
|
||||||
'pretix.event.live.activated',
|
'pretix.event.live.activated',
|
||||||
@@ -407,19 +381,6 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
'pretix.event.orders.waitinglist.voucher_assigned',
|
'pretix.event.orders.waitinglist.voucher_assigned',
|
||||||
_('Waiting list entry received voucher'),
|
_('Waiting list entry received voucher'),
|
||||||
),
|
),
|
||||||
ParametrizedVoucherWebhookEvent(
|
|
||||||
'pretix.voucher.added',
|
|
||||||
_('Voucher added'),
|
|
||||||
),
|
|
||||||
ParametrizedVoucherWebhookEvent(
|
|
||||||
'pretix.voucher.changed',
|
|
||||||
_('Voucher changed'),
|
|
||||||
_('Only includes explicit changes to the voucher, not e.g. an increase of the number of redemptions.')
|
|
||||||
),
|
|
||||||
ParametrizedVoucherWebhookEvent(
|
|
||||||
'pretix.voucher.deleted',
|
|
||||||
_('Voucher deleted'),
|
|
||||||
),
|
|
||||||
ParametrizedCustomerWebhookEvent(
|
ParametrizedCustomerWebhookEvent(
|
||||||
'pretix.customer.created',
|
'pretix.customer.created',
|
||||||
_('Customer account created'),
|
_('Customer account created'),
|
||||||
@@ -439,12 +400,8 @@ def register_default_webhook_events(sender, **kwargs):
|
|||||||
def notify_webhooks(logentry_ids: list):
|
def notify_webhooks(logentry_ids: list):
|
||||||
if not isinstance(logentry_ids, list):
|
if not isinstance(logentry_ids, list):
|
||||||
logentry_ids = [logentry_ids]
|
logentry_ids = [logentry_ids]
|
||||||
qs = LogEntry.all.select_related(
|
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer').filter(id__in=logentry_ids)
|
||||||
'event', 'event__organizer', 'organizer'
|
_org, _at, webhooks = None, None, None
|
||||||
).order_by(
|
|
||||||
'action_type', 'organizer_id', 'event_id',
|
|
||||||
).filter(id__in=logentry_ids)
|
|
||||||
_org, _at, _ev, webhooks = None, None, None, None
|
|
||||||
for logentry in qs:
|
for logentry in qs:
|
||||||
if not logentry.organizer:
|
if not logentry.organizer:
|
||||||
break # We need to know the organizer
|
break # We need to know the organizer
|
||||||
@@ -454,7 +411,7 @@ def notify_webhooks(logentry_ids: list):
|
|||||||
if not notification_type:
|
if not notification_type:
|
||||||
break # Ignore, no webhooks for this event type
|
break # Ignore, no webhooks for this event type
|
||||||
|
|
||||||
if _org != logentry.organizer or _at != logentry.action_type or _ev != logentry.event_id or webhooks is None:
|
if _org != logentry.organizer or _at != logentry.action_type or webhooks is None:
|
||||||
_org = logentry.organizer
|
_org = logentry.organizer
|
||||||
_at = logentry.action_type
|
_at = logentry.action_type
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -43,10 +43,10 @@ class PretixBaseConfig(AppConfig):
|
|||||||
from . import exporter # NOQA
|
from . import exporter # NOQA
|
||||||
from . import payment # NOQA
|
from . import payment # NOQA
|
||||||
from . import exporters # NOQA
|
from . import exporters # NOQA
|
||||||
from .invoicing import pdf, transmission, email, peppol, national # NOQA
|
from . import invoice # NOQA
|
||||||
from . import notifications # NOQA
|
from . import notifications # NOQA
|
||||||
from . import email # NOQA
|
from . import email # NOQA
|
||||||
from .services import auth, checkin, currencies, datasync, export, mail, tickets, cart, modelimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
from .services import auth, checkin, currencies, export, mail, tickets, cart, modelimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
||||||
from .models import _transactions # NOQA
|
from .models import _transactions # NOQA
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -199,7 +199,6 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
|
|||||||
params['client_id'] = provider.configuration['client_id']
|
params['client_id'] = provider.configuration['client_id']
|
||||||
params['client_secret'] = provider.configuration['client_secret']
|
params['client_secret'] = provider.configuration['client_secret']
|
||||||
|
|
||||||
resp = None
|
|
||||||
try:
|
try:
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
endpoint,
|
endpoint,
|
||||||
@@ -215,10 +214,7 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
except RequestException:
|
except RequestException:
|
||||||
if resp:
|
logger.exception('Could not retrieve authorization token')
|
||||||
logger.exception(f'Could not retrieve authorization token. Response: {resp.text}')
|
|
||||||
else:
|
|
||||||
logger.exception('Could not retrieve authorization token')
|
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Login was not successful. Error message: "{error}".').format(
|
_('Login was not successful. Error message: "{error}".').format(
|
||||||
error='could not reach login provider',
|
error='could not reach login provider',
|
||||||
@@ -226,7 +222,6 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
|
|||||||
)
|
)
|
||||||
|
|
||||||
if 'access_token' not in data:
|
if 'access_token' not in data:
|
||||||
logger.error(f'Could not find access token. Response: {data}')
|
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Login was not successful. Error message: "{error}".').format(
|
_('Login was not successful. Error message: "{error}".').format(
|
||||||
error='access token missing',
|
error='access token missing',
|
||||||
@@ -234,7 +229,6 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
|
|||||||
)
|
)
|
||||||
|
|
||||||
endpoint = provider.configuration['provider_config']['userinfo_endpoint']
|
endpoint = provider.configuration['provider_config']['userinfo_endpoint']
|
||||||
resp = None
|
|
||||||
try:
|
try:
|
||||||
# https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
# https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
@@ -246,10 +240,7 @@ def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
userinfo = resp.json()
|
userinfo = resp.json()
|
||||||
except RequestException:
|
except RequestException:
|
||||||
if resp:
|
logger.exception('Could not retrieve user info')
|
||||||
logger.exception(f'Could not retrieve user info. Response: {resp.text}')
|
|
||||||
else:
|
|
||||||
logger.exception('Could not retrieve user info')
|
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Login was not successful. Error message: "{error}".').format(
|
_('Login was not successful. Error message: "{error}".').format(
|
||||||
error='could not fetch user info',
|
error='could not fetch user info',
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
@@ -1,450 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from collections import namedtuple
|
|
||||||
from datetime import timedelta
|
|
||||||
from functools import cached_property
|
|
||||||
from typing import List, Optional, Protocol
|
|
||||||
|
|
||||||
import sentry_sdk
|
|
||||||
from django.db import DatabaseError, transaction
|
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from pretix.base.datasync.sourcefields import (
|
|
||||||
EVENT, EVENT_OR_SUBEVENT, ORDER, ORDER_POSITION, get_data_fields,
|
|
||||||
)
|
|
||||||
from pretix.base.i18n import language
|
|
||||||
from pretix.base.logentrytype_registry import make_link
|
|
||||||
from pretix.base.models.datasync import OrderSyncQueue, OrderSyncResult
|
|
||||||
from pretix.base.signals import PluginAwareRegistry
|
|
||||||
from pretix.helpers import OF_SELF
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
datasync_providers = PluginAwareRegistry({"identifier": lambda o: o.identifier})
|
|
||||||
|
|
||||||
|
|
||||||
class BaseSyncError(Exception):
|
|
||||||
def __init__(self, messages, full_message=None):
|
|
||||||
self.messages = messages
|
|
||||||
self.full_message = full_message
|
|
||||||
|
|
||||||
|
|
||||||
class UnrecoverableSyncError(BaseSyncError):
|
|
||||||
"""
|
|
||||||
A SyncProvider encountered a permanent problem, where a retry will not be successful.
|
|
||||||
"""
|
|
||||||
failure_mode = "permanent"
|
|
||||||
|
|
||||||
|
|
||||||
class SyncConfigError(UnrecoverableSyncError):
|
|
||||||
"""
|
|
||||||
A SyncProvider is misconfigured in a way where a retry without configuration change will
|
|
||||||
not be successful.
|
|
||||||
"""
|
|
||||||
failure_mode = "config"
|
|
||||||
|
|
||||||
|
|
||||||
class RecoverableSyncError(BaseSyncError):
|
|
||||||
"""
|
|
||||||
A SyncProvider has encountered a temporary problem, and the sync should be retried
|
|
||||||
at a later time.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectMapping(Protocol):
|
|
||||||
id: int
|
|
||||||
pretix_model: str
|
|
||||||
external_object_type: str
|
|
||||||
pretix_id_field: str
|
|
||||||
external_id_field: str
|
|
||||||
property_mappings: str
|
|
||||||
|
|
||||||
|
|
||||||
StaticMapping = namedtuple('StaticMapping', ('id', 'pretix_model', 'external_object_type', 'pretix_id_field', 'external_id_field', 'property_mappings'))
|
|
||||||
|
|
||||||
|
|
||||||
class OutboundSyncProvider:
|
|
||||||
max_attempts = 5
|
|
||||||
|
|
||||||
def __init__(self, event):
|
|
||||||
self.event = event
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@property
|
|
||||||
def display_name(cls):
|
|
||||||
return str(cls.identifier)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def enqueue_order(cls, order, triggered_by, not_before=None, immediate=False):
|
|
||||||
"""
|
|
||||||
Adds an order to the sync queue. May only be called on derived classes which define an ``identifier`` attribute.
|
|
||||||
|
|
||||||
Should be called in the appropriate signal receivers, e.g.::
|
|
||||||
|
|
||||||
@receiver(order_placed, dispatch_uid="mysync_order_placed")
|
|
||||||
def on_order_placed(sender, order, **kwargs):
|
|
||||||
MySyncProvider.enqueue_order(order, "order_placed")
|
|
||||||
|
|
||||||
:param order: the Order that should be synced
|
|
||||||
:param triggered_by: the reason why the order should be synced, e.g. name of the signal
|
|
||||||
(currently only used internally for logging)
|
|
||||||
:param immediate: whether a new sync task should run immediately for this order, instead
|
|
||||||
of waiting for the next periodic_task interval
|
|
||||||
:return: Return a tuple (queue_item, created), where created is a boolean
|
|
||||||
specifying whether a new queue item was created.
|
|
||||||
"""
|
|
||||||
if not hasattr(cls, 'identifier'):
|
|
||||||
raise TypeError('Call this method on a derived class that defines an "identifier" attribute.')
|
|
||||||
queue_item, created = OrderSyncQueue.objects.update_or_create(
|
|
||||||
order=order,
|
|
||||||
sync_provider=cls.identifier,
|
|
||||||
in_flight=False,
|
|
||||||
defaults={
|
|
||||||
"event": order.event,
|
|
||||||
"triggered_by": triggered_by,
|
|
||||||
"not_before": not_before or now(),
|
|
||||||
"need_manual_retry": None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if immediate:
|
|
||||||
from pretix.base.services.datasync import sync_single
|
|
||||||
sync_single.apply_async(args=(queue_item.pk,))
|
|
||||||
return queue_item, created
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_external_link_info(cls, event, external_link_href, external_link_display_name):
|
|
||||||
return {
|
|
||||||
"href": external_link_href,
|
|
||||||
"val": external_link_display_name,
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_external_link_html(cls, event, external_link_href, external_link_display_name):
|
|
||||||
info = cls.get_external_link_info(event, external_link_href, external_link_display_name)
|
|
||||||
return make_link(info, '{val}')
|
|
||||||
|
|
||||||
def next_retry_date(self, sq):
|
|
||||||
"""
|
|
||||||
Optionally override to configure a different retry backoff behavior
|
|
||||||
"""
|
|
||||||
return now() + timedelta(hours=1)
|
|
||||||
|
|
||||||
def should_sync_order(self, order):
|
|
||||||
"""
|
|
||||||
Optionally override this method to exclude certain orders from sync by returning ``False``
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mappings(self):
|
|
||||||
"""
|
|
||||||
Implementations must override this property to provide the data mappings as a list of objects.
|
|
||||||
|
|
||||||
They can return instances of the ``StaticMapping`` `namedtuple` defined above, or create their own
|
|
||||||
class (e.g. a Django model).
|
|
||||||
|
|
||||||
:return: The returned objects must have at least the following properties:
|
|
||||||
|
|
||||||
- `id`: Unique identifier for this mapping. If the mappings are Django models, the database primary key
|
|
||||||
should be used. This may be referenced in other mappings, to establish relations between objects.
|
|
||||||
- `pretix_model`: Which pretix model to use as data source in this mapping. Possible values are
|
|
||||||
the keys of ``sourcefields.AVAILABLE_MODELS``
|
|
||||||
- `external_object_type`: Destination object type in the target system. opaque string of maximum 128 characters.
|
|
||||||
- `pretix_id_field`: Which pretix data field should be used to identify the mapped object. Any ``DataFieldInfo.key``
|
|
||||||
returned by ``sourcefields.get_data_fields()`` for the combination of ``Event`` and ``pretix_model``.
|
|
||||||
- `external_id_field`: Destination identifier field in the target system.
|
|
||||||
- `property_mappings`: Mapping configuration as generated by ``PropertyMappingFormSet.to_property_mappings_list()``.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def sync_queued_orders(self, queued_orders):
|
|
||||||
"""
|
|
||||||
This method should catch all Exceptions and handle them appropriately. It should never throw
|
|
||||||
an Exception, as that may block the entire queue.
|
|
||||||
"""
|
|
||||||
for queue_item in queued_orders:
|
|
||||||
with transaction.atomic():
|
|
||||||
try:
|
|
||||||
sq = (
|
|
||||||
OrderSyncQueue.objects
|
|
||||||
.select_for_update(of=OF_SELF, nowait=True)
|
|
||||||
.select_related("order")
|
|
||||||
.get(pk=queue_item.pk)
|
|
||||||
)
|
|
||||||
if sq.in_flight:
|
|
||||||
continue
|
|
||||||
sq.in_flight = True
|
|
||||||
sq.in_flight_since = now()
|
|
||||||
sq.save()
|
|
||||||
except DatabaseError:
|
|
||||||
# Either select_for_update failed to lock the row, or we couldn't set in_flight
|
|
||||||
# as this order is already in flight (UNIQUE violation). In either case, we ignore
|
|
||||||
# this order for now.
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
mapped_objects = self.sync_order(sq.order)
|
|
||||||
if not all(all(not res or res.sync_info.get("action", "") == "nothing_to_do" for res in res_list) for res_list in mapped_objects.values()):
|
|
||||||
sq.order.log_action("pretix.event.order.data_sync.success", {
|
|
||||||
"provider": self.identifier,
|
|
||||||
"objects": {
|
|
||||||
mapping_id: [osr and osr.to_result_dict() for osr in results]
|
|
||||||
for mapping_id, results in mapped_objects.items()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
sq.delete()
|
|
||||||
except UnrecoverableSyncError as e:
|
|
||||||
sq.set_sync_error(e.failure_mode, e.messages, e.full_message)
|
|
||||||
except RecoverableSyncError as e:
|
|
||||||
sq.failed_attempts += 1
|
|
||||||
sq.not_before = self.next_retry_date(sq)
|
|
||||||
# model changes saved by set_sync_error / clear_in_flight calls below
|
|
||||||
if sq.failed_attempts >= self.max_attempts:
|
|
||||||
logger.exception('Failed to sync order (max attempts exceeded)')
|
|
||||||
sentry_sdk.capture_exception(e)
|
|
||||||
sq.set_sync_error("exceeded", e.messages, e.full_message)
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
f"Could not sync order {sq.order.code} to {type(self).__name__} "
|
|
||||||
f"(transient error, attempt #{sq.failed_attempts}, next {sq.not_before})",
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
sq.clear_in_flight()
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception('Failed to sync order (unhandled exception)')
|
|
||||||
sentry_sdk.capture_exception(e)
|
|
||||||
sq.set_sync_error("internal", [], str(e))
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def data_fields(self):
|
|
||||||
return {
|
|
||||||
f.key: f
|
|
||||||
for f in get_data_fields(self.event)
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_field_value(self, inputs, mapping_entry):
|
|
||||||
key = mapping_entry["pretix_field"]
|
|
||||||
try:
|
|
||||||
field = self.data_fields[key]
|
|
||||||
except KeyError:
|
|
||||||
with language(self.event.settings.locale):
|
|
||||||
raise SyncConfigError([_(
|
|
||||||
'Field "{field_name}" does not exist. Please check your {provider_name} settings.'
|
|
||||||
).format(field_name=key, provider_name=self.display_name)])
|
|
||||||
try:
|
|
||||||
input = inputs[field.required_input]
|
|
||||||
except KeyError:
|
|
||||||
with language(self.event.settings.locale):
|
|
||||||
raise SyncConfigError([_(
|
|
||||||
'Field "{field_name}" requires {required_input}, but only got {available_inputs}. Please check your {provider_name} settings.'
|
|
||||||
).format(field_name=key, required_input=field.required_input, available_inputs=", ".join(inputs.keys()), provider_name=self.display_name)])
|
|
||||||
val = field.getter(input)
|
|
||||||
if isinstance(val, list):
|
|
||||||
if field.enum_opts and mapping_entry.get("value_map"):
|
|
||||||
map = json.loads(mapping_entry["value_map"])
|
|
||||||
try:
|
|
||||||
val = [map[el] for el in val]
|
|
||||||
except KeyError:
|
|
||||||
with language(self.event.settings.locale):
|
|
||||||
raise SyncConfigError([_(
|
|
||||||
'Please update value mapping for field "{field_name}" - option "{val}" not assigned'
|
|
||||||
).format(field_name=key, val=val)])
|
|
||||||
|
|
||||||
val = ",".join(val)
|
|
||||||
return val
|
|
||||||
|
|
||||||
def get_properties(self, inputs: dict, property_mappings: List[dict]):
|
|
||||||
return [
|
|
||||||
(m["external_field"], self.get_field_value(inputs, m), m["overwrite"])
|
|
||||||
for m in property_mappings
|
|
||||||
]
|
|
||||||
|
|
||||||
def sync_object_with_properties(
|
|
||||||
self,
|
|
||||||
external_id_field: str,
|
|
||||||
id_value,
|
|
||||||
properties: list,
|
|
||||||
inputs: dict,
|
|
||||||
mapping: ObjectMapping,
|
|
||||||
mapped_objects: dict,
|
|
||||||
**kwargs,
|
|
||||||
) -> Optional[dict]:
|
|
||||||
"""
|
|
||||||
This method is called for each object that needs to be created/updated in the external system -- which these are is
|
|
||||||
determined by the implementation of the `mapping` property.
|
|
||||||
|
|
||||||
:param external_id_field: Identifier field in the external system as provided in ``mapping.external_identifier``
|
|
||||||
:param id_value: Identifier contents as retrieved from the property specified by ``mapping.pretix_identifier`` of the model
|
|
||||||
specified by ``mapping.pretix_model``
|
|
||||||
:param properties: All properties defined in ``mapping.property_mappings``, as list of three-tuples
|
|
||||||
``(external_field, value, overwrite)``
|
|
||||||
:param inputs: All pretix model instances from which data can be retrieved for this mapping.
|
|
||||||
Dictionary mapping from sourcefields.ORDER_POSITION, .ORDER, .EVENT, .EVENT_OR_SUBEVENT to the
|
|
||||||
relevant Django model.
|
|
||||||
Most providers don't need to use this parameter directly, as `properties` and `id_value`
|
|
||||||
already contain the values as evaluated from the available inputs.
|
|
||||||
:param mapping: The mapping object as returned by ``self.mappings``
|
|
||||||
:param mapped_objects: Information about objects that were synced in the same sync run, by mapping definitions
|
|
||||||
*before* the current one in order of ``self.mappings``.
|
|
||||||
Type is a dictionary ``{mapping.id: [list of OrderSyncResult objects]}``
|
|
||||||
Useful to create associations between objects in the target system.
|
|
||||||
|
|
||||||
Example code to create return value::
|
|
||||||
|
|
||||||
return {
|
|
||||||
# optional:
|
|
||||||
"action": "nothing_to_do", # to inform that no action was taken, because the data was already up-to-date.
|
|
||||||
# other values for action (e.g. create, update) currently have no special
|
|
||||||
# meaning, but are visible for debugging purposes to admins.
|
|
||||||
|
|
||||||
# optional:
|
|
||||||
"external_link_href": "https://external-system.example.com/backend/link/to/contact/123/",
|
|
||||||
"external_link_display_name": "Contact #123 - Jane Doe",
|
|
||||||
"...optionally further values you need in mapped_objects for association": 123456789,
|
|
||||||
}
|
|
||||||
|
|
||||||
The return value needs to be a JSON serializable dict, or None.
|
|
||||||
|
|
||||||
Return None only in case you decide this object should not be synced at all in this mapping. Do not return None in
|
|
||||||
case the object is already up-to-date in the target system (return "action": "nothing_to_do" instead).
|
|
||||||
|
|
||||||
This method needs to be idempotent, i.e. calling it multiple times with the same input values should create
|
|
||||||
only a single object in the target system.
|
|
||||||
|
|
||||||
Subsequent calls with the same mapping and id_value should update the existing object, instead of creating a new one.
|
|
||||||
In a SQL database, you might use an `INSERT OR UPDATE` or `UPSERT` statement; many REST APIs provide an equivalent API call.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def sync_object(
|
|
||||||
self,
|
|
||||||
inputs: dict,
|
|
||||||
mapping,
|
|
||||||
mapped_objects: dict,
|
|
||||||
):
|
|
||||||
logger.debug("Syncing object %r, %r, %r", inputs, mapping, mapped_objects)
|
|
||||||
properties = self.get_properties(inputs, mapping.property_mappings)
|
|
||||||
logger.debug("Properties: %r", properties)
|
|
||||||
|
|
||||||
id_value = self.get_field_value(inputs, {"pretix_field": mapping.pretix_id_field})
|
|
||||||
if not id_value:
|
|
||||||
return None
|
|
||||||
|
|
||||||
info = self.sync_object_with_properties(
|
|
||||||
external_id_field=mapping.external_id_field,
|
|
||||||
id_value=id_value,
|
|
||||||
properties=properties,
|
|
||||||
inputs=inputs,
|
|
||||||
mapping=mapping,
|
|
||||||
mapped_objects=mapped_objects,
|
|
||||||
)
|
|
||||||
if not info:
|
|
||||||
return None
|
|
||||||
external_link_href = info.pop('external_link_href', None)
|
|
||||||
external_link_display_name = info.pop('external_link_display_name', None)
|
|
||||||
obj, created = OrderSyncResult.objects.update_or_create(
|
|
||||||
order=inputs.get(ORDER), order_position=inputs.get(ORDER_POSITION), sync_provider=self.identifier,
|
|
||||||
mapping_id=mapping.id,
|
|
||||||
defaults=dict(
|
|
||||||
external_object_type=mapping.external_object_type,
|
|
||||||
external_id_field=mapping.external_id_field,
|
|
||||||
id_value=id_value,
|
|
||||||
external_link_href=external_link_href,
|
|
||||||
external_link_display_name=external_link_display_name,
|
|
||||||
sync_info=info,
|
|
||||||
transmitted=now(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def sync_order(self, order):
|
|
||||||
if not self.should_sync_order(order):
|
|
||||||
logger.debug("Skipping order %r", order)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
logger.debug("Syncing order %r", order)
|
|
||||||
positions = list(
|
|
||||||
order.all_positions
|
|
||||||
.prefetch_related("answers", "answers__question")
|
|
||||||
.select_related(
|
|
||||||
"voucher",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
order_inputs = {ORDER: order, EVENT: self.event}
|
|
||||||
mapped_objects = {}
|
|
||||||
for mapping in self.mappings:
|
|
||||||
if mapping.pretix_model == 'Order':
|
|
||||||
mapped_objects[mapping.id] = [
|
|
||||||
self.sync_object(order_inputs, mapping, mapped_objects)
|
|
||||||
]
|
|
||||||
elif mapping.pretix_model == 'OrderPosition':
|
|
||||||
mapped_objects[mapping.id] = [
|
|
||||||
self.sync_object({
|
|
||||||
**order_inputs, EVENT_OR_SUBEVENT: op.subevent or self.event, ORDER_POSITION: op
|
|
||||||
}, mapping, mapped_objects)
|
|
||||||
for op in positions
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
raise SyncConfigError("Invalid pretix model '{}'".format(mapping.pretix_model))
|
|
||||||
self.finalize_sync_order(order)
|
|
||||||
return mapped_objects
|
|
||||||
|
|
||||||
def filter_mapped_objects(self, mapped_objects, inputs):
|
|
||||||
"""
|
|
||||||
For order positions, only
|
|
||||||
"""
|
|
||||||
if ORDER_POSITION in inputs:
|
|
||||||
return {
|
|
||||||
mapping_id: [
|
|
||||||
osr for osr in results
|
|
||||||
if osr and (osr.order_position_id is None or osr.order_position_id == inputs[ORDER_POSITION].id)
|
|
||||||
]
|
|
||||||
for mapping_id, results in mapped_objects.items()
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return mapped_objects
|
|
||||||
|
|
||||||
def finalize_sync_order(self, order):
|
|
||||||
"""
|
|
||||||
Called after ``sync_object`` has been called successfully for all objects of a specific order. Can
|
|
||||||
be used for saving bulk information per order.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""
|
|
||||||
Called after all orders of an event have been synced. Can be used for clean-up tasks (e.g. closing
|
|
||||||
a session).
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
@@ -1,659 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
|
|
||||||
from collections import namedtuple
|
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
from django.db.models import Max, Q
|
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
|
||||||
|
|
||||||
from pretix.base.models import Checkin, InvoiceAddress, Order, Question
|
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
|
||||||
|
|
||||||
|
|
||||||
def get_answer(op, question_identifier=None):
|
|
||||||
a = None
|
|
||||||
if op.addon_to:
|
|
||||||
if "answers" in getattr(op.addon_to, "_prefetched_objects_cache", {}):
|
|
||||||
try:
|
|
||||||
a = [
|
|
||||||
a
|
|
||||||
for a in op.addon_to.answers.all()
|
|
||||||
if a.question.identifier == question_identifier
|
|
||||||
][0]
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
a = op.addon_to.answers.filter(
|
|
||||||
question__identifier=question_identifier
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if "answers" in getattr(op, "_prefetched_objects_cache", {}):
|
|
||||||
try:
|
|
||||||
a = [
|
|
||||||
a
|
|
||||||
for a in op.answers.all()
|
|
||||||
if a.question.identifier == question_identifier
|
|
||||||
][0]
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
a = op.answers.filter(question__identifier=question_identifier).first()
|
|
||||||
|
|
||||||
if not a:
|
|
||||||
return ""
|
|
||||||
else:
|
|
||||||
if a.question.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
|
||||||
return [str(o.identifier) for o in a.options.all()]
|
|
||||||
if a.question.type == Question.TYPE_BOOLEAN:
|
|
||||||
return a.answer == "True"
|
|
||||||
return a.answer
|
|
||||||
|
|
||||||
|
|
||||||
def get_payment_date(order):
|
|
||||||
if order.status == Order.STATUS_PENDING:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return isoformat_or_none(order.payments.aggregate(m=Max("payment_date"))["m"])
|
|
||||||
|
|
||||||
|
|
||||||
def isoformat_or_none(dt):
|
|
||||||
return dt and dt.isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
def first_checkin_on_list(list_pk, position):
|
|
||||||
checkin = position.checkins.filter(
|
|
||||||
list__pk=list_pk, type=Checkin.TYPE_ENTRY
|
|
||||||
).first()
|
|
||||||
if checkin:
|
|
||||||
return isoformat_or_none(checkin.datetime)
|
|
||||||
|
|
||||||
|
|
||||||
def split_name_on_last_space(name, part):
|
|
||||||
name_parts = name.rsplit(" ", 1)
|
|
||||||
return name_parts[part] if len(name_parts) > part else ""
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_email(email):
|
|
||||||
if email:
|
|
||||||
local, host = email.split("@")
|
|
||||||
host = host.encode("idna").decode()
|
|
||||||
return f"{local}@{host}"
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_email_domain(email):
|
|
||||||
if email:
|
|
||||||
local, host = email.split("@")
|
|
||||||
return host
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
ORDER_POSITION = 'position'
|
|
||||||
ORDER = 'order'
|
|
||||||
EVENT = 'event'
|
|
||||||
EVENT_OR_SUBEVENT = 'event_or_subevent'
|
|
||||||
AVAILABLE_MODELS = {
|
|
||||||
'OrderPosition': (ORDER_POSITION, ORDER, EVENT_OR_SUBEVENT, EVENT),
|
|
||||||
'Order': (ORDER, EVENT),
|
|
||||||
}
|
|
||||||
|
|
||||||
DataFieldCategory = namedtuple(
|
|
||||||
'DataFieldCategory',
|
|
||||||
field_names=('sort_index', 'label',),
|
|
||||||
)
|
|
||||||
|
|
||||||
CAT_ORDER_POSITION = DataFieldCategory(10, _('Order position details'))
|
|
||||||
CAT_ATTENDEE = DataFieldCategory(11, _('Attendee details'))
|
|
||||||
CAT_QUESTIONS = DataFieldCategory(12, _('Questions'))
|
|
||||||
CAT_PRODUCT = DataFieldCategory(20, _('Product details'))
|
|
||||||
CAT_ORDER = DataFieldCategory(21, _('Order details'))
|
|
||||||
CAT_INVOICE_ADDRESS = DataFieldCategory(22, _('Invoice address'))
|
|
||||||
CAT_EVENT = DataFieldCategory(30, _('Event information'))
|
|
||||||
CAT_EVENT_OR_SUBEVENT = DataFieldCategory(31, pgettext_lazy('subevent', 'Event or date information'))
|
|
||||||
|
|
||||||
DataFieldInfo = namedtuple(
|
|
||||||
'DataFieldInfo',
|
|
||||||
field_names=('required_input', 'category', 'key', 'label', 'type', 'enum_opts', 'getter', 'deprecated'),
|
|
||||||
defaults=[False]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_invoice_address_or_empty(order):
|
|
||||||
try:
|
|
||||||
return order.invoice_address
|
|
||||||
except InvoiceAddress.DoesNotExist:
|
|
||||||
return InvoiceAddress()
|
|
||||||
|
|
||||||
|
|
||||||
def get_data_fields(event, for_model=None):
|
|
||||||
"""
|
|
||||||
Returns tuple of (required_input, key, label, type, enum_opts, getter)
|
|
||||||
|
|
||||||
Type is one of the Question types as defined in Question.TYPE_CHOICES.
|
|
||||||
|
|
||||||
The data type of the return value of `getter` depends on `type`:
|
|
||||||
- TYPE_CHOICE_MULTIPLE: list of strings
|
|
||||||
- TYPE_CHOICE: list, containing zero or one strings
|
|
||||||
- TYPE_BOOLEAN: boolean
|
|
||||||
- all other (including TYPE_NUMBER): string
|
|
||||||
"""
|
|
||||||
name_scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
|
|
||||||
name_headers = []
|
|
||||||
if name_scheme and len(name_scheme["fields"]) > 1:
|
|
||||||
for k, label, w in name_scheme["fields"]:
|
|
||||||
name_headers.append(label)
|
|
||||||
|
|
||||||
src_fields = (
|
|
||||||
[
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ATTENDEE,
|
|
||||||
"attendee_name",
|
|
||||||
_("Attendee name"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda position: position.attendee_name
|
|
||||||
or (position.addon_to.attendee_name if position.addon_to else None),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
+ [
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ATTENDEE,
|
|
||||||
"attendee_name_" + k,
|
|
||||||
_("Attendee") + ": " + label,
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
partial(
|
|
||||||
lambda k, position: (
|
|
||||||
position.attendee_name_parts
|
|
||||||
or (position.addon_to.attendee_name_parts if position.addon_to else {})
|
|
||||||
or {}
|
|
||||||
).get(k, ""),
|
|
||||||
k,
|
|
||||||
),
|
|
||||||
deprecated=len(name_scheme["fields"]) == 1,
|
|
||||||
)
|
|
||||||
for k, label, w in name_scheme["fields"]
|
|
||||||
]
|
|
||||||
+ [
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ATTENDEE,
|
|
||||||
"attendee_email",
|
|
||||||
_("Attendee email"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda position: normalize_email(
|
|
||||||
position.attendee_email
|
|
||||||
or (position.addon_to.attendee_email if position.addon_to else None)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ATTENDEE,
|
|
||||||
"attendee_or_order_email",
|
|
||||||
_("Attendee or order email"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda position: normalize_email(
|
|
||||||
position.attendee_email
|
|
||||||
or (position.addon_to.attendee_email if position.addon_to else None)
|
|
||||||
or position.order.email
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ATTENDEE,
|
|
||||||
"attendee_company",
|
|
||||||
_("Attendee company"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda position: position.company or (position.addon_to.company if position.addon_to else None),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ATTENDEE,
|
|
||||||
"attendee_street",
|
|
||||||
_("Attendee address street"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda position: position.street or (position.addon_to.street if position.addon_to else None),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ATTENDEE,
|
|
||||||
"attendee_zipcode",
|
|
||||||
_("Attendee address ZIP code"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda position: position.zipcode or (position.addon_to.zipcode if position.addon_to else None),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ATTENDEE,
|
|
||||||
"attendee_city",
|
|
||||||
_("Attendee address city"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda position: position.city or (position.addon_to.city if position.addon_to else None),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ATTENDEE,
|
|
||||||
"attendee_country",
|
|
||||||
_("Attendee address country"),
|
|
||||||
Question.TYPE_COUNTRYCODE,
|
|
||||||
None,
|
|
||||||
lambda position: str(
|
|
||||||
position.country or (position.addon_to.attendee_name if position.addon_to else "")
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_INVOICE_ADDRESS,
|
|
||||||
"invoice_address_company",
|
|
||||||
_("Invoice address company"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda order: get_invoice_address_or_empty(order).company,
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_INVOICE_ADDRESS,
|
|
||||||
"invoice_address_name",
|
|
||||||
_("Invoice address name"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda order: get_invoice_address_or_empty(order).name,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
+ [
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_INVOICE_ADDRESS,
|
|
||||||
"invoice_address_name_" + k,
|
|
||||||
_("Invoice address") + ": " + label,
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
partial(
|
|
||||||
lambda k, order: (get_invoice_address_or_empty(order).name_parts or {}).get(
|
|
||||||
k, ""
|
|
||||||
),
|
|
||||||
k,
|
|
||||||
),
|
|
||||||
deprecated=len(name_scheme["fields"]) == 1,
|
|
||||||
)
|
|
||||||
for k, label, w in name_scheme["fields"]
|
|
||||||
]
|
|
||||||
+ [
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_INVOICE_ADDRESS,
|
|
||||||
"invoice_address_street",
|
|
||||||
_("Invoice address street"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda order: get_invoice_address_or_empty(order).street,
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_INVOICE_ADDRESS,
|
|
||||||
"invoice_address_zipcode",
|
|
||||||
_("Invoice address ZIP code"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda order: get_invoice_address_or_empty(order).zipcode,
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_INVOICE_ADDRESS,
|
|
||||||
"invoice_address_city",
|
|
||||||
_("Invoice address city"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda order: get_invoice_address_or_empty(order).city,
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_INVOICE_ADDRESS,
|
|
||||||
"invoice_address_country",
|
|
||||||
_("Invoice address country"),
|
|
||||||
Question.TYPE_COUNTRYCODE,
|
|
||||||
None,
|
|
||||||
lambda order: str(get_invoice_address_or_empty(order).country),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_ORDER,
|
|
||||||
"email",
|
|
||||||
_("Order email"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda order: normalize_email(order.email),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_ORDER,
|
|
||||||
"email_domain",
|
|
||||||
_("Order email domain"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda order: get_email_domain(normalize_email(order.email)),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_ORDER,
|
|
||||||
"order_code",
|
|
||||||
_("Order code"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda order: order.code,
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_ORDER,
|
|
||||||
"event_order_code",
|
|
||||||
_("Event and order code"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda order: order.full_code,
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_ORDER,
|
|
||||||
"order_total",
|
|
||||||
_("Order total"),
|
|
||||||
Question.TYPE_NUMBER,
|
|
||||||
None,
|
|
||||||
lambda order: str(order.total),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_PRODUCT,
|
|
||||||
"product",
|
|
||||||
_("Product and variation name"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda position: str(
|
|
||||||
str(position.item.internal_name or position.item.name)
|
|
||||||
+ ((" – " + str(position.variation.value)) if position.variation else "")
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_PRODUCT,
|
|
||||||
"product_id",
|
|
||||||
_("Product ID"),
|
|
||||||
Question.TYPE_NUMBER,
|
|
||||||
None,
|
|
||||||
lambda position: str(position.item.pk),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_PRODUCT,
|
|
||||||
"product_is_admission",
|
|
||||||
_("Product is admission product"),
|
|
||||||
Question.TYPE_BOOLEAN,
|
|
||||||
None,
|
|
||||||
lambda position: bool(position.item.admission),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
EVENT,
|
|
||||||
CAT_EVENT,
|
|
||||||
"event_slug",
|
|
||||||
_("Event short form"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda event: str(event.slug),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
EVENT,
|
|
||||||
CAT_EVENT,
|
|
||||||
"event_name",
|
|
||||||
_("Event name"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda event: str(event.name),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
EVENT_OR_SUBEVENT,
|
|
||||||
CAT_EVENT_OR_SUBEVENT,
|
|
||||||
"event_date_from",
|
|
||||||
_("Event start date"),
|
|
||||||
Question.TYPE_DATETIME,
|
|
||||||
None,
|
|
||||||
lambda event_or_subevent: isoformat_or_none(event_or_subevent.date_from),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
EVENT_OR_SUBEVENT,
|
|
||||||
CAT_EVENT_OR_SUBEVENT,
|
|
||||||
"event_date_to",
|
|
||||||
_("Event end date"),
|
|
||||||
Question.TYPE_DATETIME,
|
|
||||||
None,
|
|
||||||
lambda event_or_subevent: isoformat_or_none(event_or_subevent.date_to),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ORDER_POSITION,
|
|
||||||
"voucher_code",
|
|
||||||
_("Voucher code"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda position: position.voucher.code if position.voucher_id else "",
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ORDER_POSITION,
|
|
||||||
"ticket_id",
|
|
||||||
_("Order code and position number"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda position: position.code,
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ORDER_POSITION,
|
|
||||||
"ticket_price",
|
|
||||||
_("Ticket price"),
|
|
||||||
Question.TYPE_NUMBER,
|
|
||||||
None,
|
|
||||||
lambda position: str(position.price),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_ORDER,
|
|
||||||
"order_status",
|
|
||||||
_("Order status"),
|
|
||||||
Question.TYPE_CHOICE,
|
|
||||||
Order.STATUS_CHOICE,
|
|
||||||
lambda order: [order.status],
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ORDER_POSITION,
|
|
||||||
"ticket_status",
|
|
||||||
_("Ticket status"),
|
|
||||||
Question.TYPE_CHOICE,
|
|
||||||
Order.STATUS_CHOICE,
|
|
||||||
lambda position: [Order.STATUS_CANCELED if position.canceled else position.order.status],
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_ORDER,
|
|
||||||
"order_date",
|
|
||||||
_("Order date and time"),
|
|
||||||
Question.TYPE_DATETIME,
|
|
||||||
None,
|
|
||||||
lambda order: order.datetime.isoformat(),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_ORDER,
|
|
||||||
"payment_date",
|
|
||||||
_("Payment date and time"),
|
|
||||||
Question.TYPE_DATETIME,
|
|
||||||
None,
|
|
||||||
get_payment_date,
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_ORDER,
|
|
||||||
"order_locale",
|
|
||||||
_("Order locale"),
|
|
||||||
Question.TYPE_CHOICE,
|
|
||||||
[(lc, lc) for lc in event.settings.locales],
|
|
||||||
lambda order: [order.locale],
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ORDER_POSITION,
|
|
||||||
"position_id",
|
|
||||||
_("Order position ID"),
|
|
||||||
Question.TYPE_NUMBER,
|
|
||||||
None,
|
|
||||||
lambda op: str(op.pk),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_ORDER,
|
|
||||||
"presale_order_url",
|
|
||||||
_("Order link"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda order: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order', kwargs={
|
|
||||||
'order': order.code,
|
|
||||||
'secret': order.secret,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ORDER_POSITION,
|
|
||||||
"presale_ticket_url",
|
|
||||||
_("Ticket link"),
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda op: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.position', kwargs={
|
|
||||||
'order': op.order.code,
|
|
||||||
'secret': op.web_secret,
|
|
||||||
'position': op.positionid
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
+ [
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ORDER_POSITION,
|
|
||||||
"checkin_date_" + str(cl.pk),
|
|
||||||
_("Check-in datetime on list {}").format(cl.name),
|
|
||||||
Question.TYPE_DATETIME,
|
|
||||||
None,
|
|
||||||
partial(first_checkin_on_list, cl.pk),
|
|
||||||
)
|
|
||||||
for cl in event.checkin_lists.all()
|
|
||||||
]
|
|
||||||
+ [
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_QUESTIONS,
|
|
||||||
"question_" + q.identifier,
|
|
||||||
_("Question: {name}").format(name=str(q.question)),
|
|
||||||
q.type,
|
|
||||||
get_enum_opts(q),
|
|
||||||
partial(lambda qq, position: get_answer(position, qq.identifier), q),
|
|
||||||
)
|
|
||||||
for q in event.questions.filter(~Q(type=Question.TYPE_FILE)).prefetch_related("options")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
if not any(field_name == "given_name" for field_name, label, weight in name_scheme["fields"]):
|
|
||||||
src_fields += [
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ATTENDEE,
|
|
||||||
"attendee_name_given_name",
|
|
||||||
_("Attendee") + ": " + _("Given name") + " (⚠️ auto-generated, not recommended)",
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda position: split_name_on_last_space(position.attendee_name, part=0),
|
|
||||||
deprecated=True,
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_INVOICE_ADDRESS,
|
|
||||||
"invoice_address_name_given_name",
|
|
||||||
_("Invoice address") + ": " + _("Given name") + " (⚠️ auto-generated, not recommended)",
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda order: split_name_on_last_space(get_invoice_address_or_empty(order).name, part=0),
|
|
||||||
deprecated=True,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
if not any(field_name == "family_name" for field_name, label, weight in name_scheme["fields"]):
|
|
||||||
src_fields += [
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER_POSITION,
|
|
||||||
CAT_ATTENDEE,
|
|
||||||
"attendee_name_family_name",
|
|
||||||
_("Attendee") + ": " + _("Family name") + " (⚠️ auto-generated, not recommended)",
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda position: split_name_on_last_space(position.attendee_name, part=1),
|
|
||||||
deprecated=True,
|
|
||||||
),
|
|
||||||
DataFieldInfo(
|
|
||||||
ORDER,
|
|
||||||
CAT_INVOICE_ADDRESS,
|
|
||||||
"invoice_address_name_family_name",
|
|
||||||
_("Invoice address") + ": " + _("Family name") + " (⚠️ auto-generated, not recommended)",
|
|
||||||
Question.TYPE_STRING,
|
|
||||||
None,
|
|
||||||
lambda order: split_name_on_last_space(get_invoice_address_or_empty(order).name, part=1),
|
|
||||||
deprecated=True,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
if for_model:
|
|
||||||
available_inputs = AVAILABLE_MODELS[for_model]
|
|
||||||
return [
|
|
||||||
f for f in src_fields if f.required_input in available_inputs
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
return src_fields
|
|
||||||
|
|
||||||
|
|
||||||
def get_enum_opts(q):
|
|
||||||
if q.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
|
||||||
return [(opt.identifier, opt.answer) for opt in q.options.all()]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from typing import List, Tuple
|
|
||||||
|
|
||||||
from pretix.base.datasync.datasync import SyncConfigError
|
|
||||||
from pretix.base.models.datasync import (
|
|
||||||
MODE_APPEND_LIST, MODE_OVERWRITE, MODE_SET_IF_EMPTY, MODE_SET_IF_NEW,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def assign_properties(
|
|
||||||
new_values: List[Tuple[str, str, str]], old_values: dict, is_new, list_sep
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Generates a dictionary mapping property keys to new values, handling conditional overwrites and list updates
|
|
||||||
according to an update mode specified per property.
|
|
||||||
|
|
||||||
Supported update modes are:
|
|
||||||
- `MODE_OVERWRITE`: Replaces the existing value with the new value.
|
|
||||||
- `MODE_SET_IF_NEW`: Only sets the property if `is_new` is True.
|
|
||||||
- `MODE_SET_IF_EMPTY`: Only sets the property if the field is empty or missing in old_values.
|
|
||||||
- `MODE_APPEND_LIST`: Appends the new value to the list from old_values (or the empty list if missing),
|
|
||||||
using `list_sep` as a separator.
|
|
||||||
|
|
||||||
:param new_values: List of tuples, where each tuple contains (field_name, new_value, update_mode).
|
|
||||||
:param old_values: Dictionary, current property values in the external system.
|
|
||||||
:param is_new: Boolean, whether the object will be newly created in the external system.
|
|
||||||
:param list_sep: If string, used as a separator for MODE_APPEND_LIST. If None, native lists are used.
|
|
||||||
:raises SyncConfigError: If an invalid update mode is specified.
|
|
||||||
:returns: A dictionary containing the properties that need to be updated in the external system.
|
|
||||||
"""
|
|
||||||
|
|
||||||
out = {}
|
|
||||||
|
|
||||||
for field_name, new_value, update_mode in new_values:
|
|
||||||
if update_mode == MODE_OVERWRITE:
|
|
||||||
out[field_name] = new_value
|
|
||||||
continue
|
|
||||||
elif update_mode == MODE_SET_IF_NEW and not is_new:
|
|
||||||
continue
|
|
||||||
if not new_value:
|
|
||||||
continue
|
|
||||||
|
|
||||||
current_value = old_values.get(field_name, out.get(field_name, ""))
|
|
||||||
if update_mode in (MODE_SET_IF_EMPTY, MODE_SET_IF_NEW):
|
|
||||||
if not current_value:
|
|
||||||
out[field_name] = new_value
|
|
||||||
elif update_mode == MODE_APPEND_LIST:
|
|
||||||
_add_to_list(out, field_name, current_value, new_value, list_sep)
|
|
||||||
else:
|
|
||||||
raise SyncConfigError(["Invalid update mode " + update_mode])
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _add_to_list(out, field_name, current_value, new_item, list_sep):
|
|
||||||
new_item = str(new_item)
|
|
||||||
if list_sep is not None:
|
|
||||||
new_item = new_item.replace(list_sep, "")
|
|
||||||
current_value = current_value.split(list_sep) if current_value else []
|
|
||||||
elif not isinstance(current_value, (list, tuple)):
|
|
||||||
current_value = [str(current_value)]
|
|
||||||
if new_item not in current_value:
|
|
||||||
new_list = current_value + [new_item]
|
|
||||||
if list_sep is not None:
|
|
||||||
new_list = list_sep.join(new_list)
|
|
||||||
out[field_name] = new_list
|
|
||||||
|
|
||||||
|
|
||||||
def translate_property_mappings(property_mappings, checkin_list_map):
|
|
||||||
"""
|
|
||||||
To properly handle copied events, users of data fields as provided by get_data_fields need to register to the
|
|
||||||
event_copy_data signal and translate all stored references to those fields using this method.
|
|
||||||
|
|
||||||
For example, if you store your mappings in a custom Django model with a ForeignKey to Event:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@receiver(signal=event_copy_data, dispatch_uid="my_sync_event_copy_data")
|
|
||||||
def event_copy_data_receiver(sender, other, checkin_list_map, **kwargs):
|
|
||||||
object_mappings = other.my_object_mappings.all()
|
|
||||||
object_mapping_map = {}
|
|
||||||
for om in object_mappings:
|
|
||||||
om = copy.copy(om)
|
|
||||||
object_mapping_map[om.pk] = om
|
|
||||||
om.pk = None
|
|
||||||
om.event = sender
|
|
||||||
om.property_mappings = translate_property_mappings(om.property_mappings, checkin_list_map)
|
|
||||||
om.save()
|
|
||||||
|
|
||||||
"""
|
|
||||||
mappings = []
|
|
||||||
|
|
||||||
for mapping in property_mappings:
|
|
||||||
pretix_field = mapping["pretix_field"]
|
|
||||||
if pretix_field.startswith("checkin_date_"):
|
|
||||||
old_id = int(pretix_field[len("checkin_date_"):])
|
|
||||||
if old_id not in checkin_list_map:
|
|
||||||
# old_id might not be in checkin_list_map, because copying of an event series only copies check-in
|
|
||||||
# lists covering the whole series, not individual dates.
|
|
||||||
pretix_field = "_invalid_" + pretix_field
|
|
||||||
else:
|
|
||||||
pretix_field = "checkin_date_%d" % checkin_list_map[old_id].pk
|
|
||||||
mappings.append({**mapping, "pretix_field": pretix_field})
|
|
||||||
return mappings
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user