forked from CGM_Public/pretix_original
Compare commits
114 Commits
release/3.
...
v3.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d29b8eba01 | ||
|
|
f566b353f2 | ||
|
|
c2221fad32 | ||
|
|
06a8a804f4 | ||
|
|
5832429540 | ||
|
|
7913de971c | ||
|
|
9b91e3e4f6 | ||
|
|
402730df43 | ||
|
|
c1d6d9bf1a | ||
|
|
ddbe27f351 | ||
|
|
35f2b10069 | ||
|
|
0d2a534982 | ||
|
|
a1ad00a30c | ||
|
|
07d2463960 | ||
|
|
08de722525 | ||
|
|
891e740ede | ||
|
|
87645a0b1f | ||
|
|
d7a575683e | ||
|
|
ca83a44489 | ||
|
|
40da03f979 | ||
|
|
fdd45a85f0 | ||
|
|
47579d0517 | ||
|
|
8704a7f3dd | ||
|
|
244e0695b1 | ||
|
|
8e2821b398 | ||
|
|
b738e3bd9d | ||
|
|
fa224fd17e | ||
|
|
76359c859f | ||
|
|
ff98ae3200 | ||
|
|
2ffb4edee9 | ||
|
|
902f589ee6 | ||
|
|
2a6dc22d7b | ||
|
|
4d9ec7c8e4 | ||
|
|
b84a0e93ae | ||
|
|
534f09bdc6 | ||
|
|
370aa63ead | ||
|
|
fb7e859e72 | ||
|
|
80a7c45e05 | ||
|
|
38a19bb58b | ||
|
|
19873e2a09 | ||
|
|
eb7e938af6 | ||
|
|
614c40596f | ||
|
|
4db56e218e | ||
|
|
ed9b96a41c | ||
|
|
e839dbc7d4 | ||
|
|
982fb0149d | ||
|
|
4597cb9849 | ||
|
|
9f629fc1c9 | ||
|
|
387e1b4998 | ||
|
|
84415864e5 | ||
|
|
82feca6e38 | ||
|
|
28242e52aa | ||
|
|
488ee19b11 | ||
|
|
ba4f00cfc0 | ||
|
|
eb392ebf20 | ||
|
|
3cef690779 | ||
|
|
294fc4735a | ||
|
|
6d17cad529 | ||
|
|
668380cc3f | ||
|
|
3bf8de39a0 | ||
|
|
1986cdf4b9 | ||
|
|
577729a271 | ||
|
|
9033e5b6f7 | ||
|
|
c1fa0d1559 | ||
|
|
e1a4dd6e43 | ||
|
|
089a468a5d | ||
|
|
018d345008 | ||
|
|
529e2a0286 | ||
|
|
1d36ef3c24 | ||
|
|
282ad2c869 | ||
|
|
e67ff83378 | ||
|
|
21be22e489 | ||
|
|
3da79ad36b | ||
|
|
8a17fedaa6 | ||
|
|
7d6b3e7140 | ||
|
|
f80ba365a5 | ||
|
|
8156cdd539 | ||
|
|
cd55146867 | ||
|
|
3cb7482bae | ||
|
|
99f3db04a9 | ||
|
|
352942b7d6 | ||
|
|
6d3ccc0182 | ||
|
|
49b73fc096 | ||
|
|
24b931e1c3 | ||
|
|
1c99e01af9 | ||
|
|
66183e805e | ||
|
|
d33c9332c6 | ||
|
|
2284def607 | ||
|
|
15c25a5a0d | ||
|
|
cf5ac6af4b | ||
|
|
2a929200b5 | ||
|
|
3f77d34026 | ||
|
|
a395b24b80 | ||
|
|
984ef60099 | ||
|
|
5b6f0df963 | ||
|
|
509c7d98cc | ||
|
|
3bd4959efe | ||
|
|
4faaa8e521 | ||
|
|
0e8832fd54 | ||
|
|
4faa76d9c7 | ||
|
|
8d1f9bf0f3 | ||
|
|
4afef62cbd | ||
|
|
3d5cfdd9c7 | ||
|
|
b3b1d09690 | ||
|
|
381ecd6d75 | ||
|
|
a12fea71e5 | ||
|
|
a6dd6ac537 | ||
|
|
c3041aa8c4 | ||
|
|
e275677a0a | ||
|
|
fff14c31ba | ||
|
|
a74bde60eb | ||
|
|
12b9d23efb | ||
|
|
afec39ce57 | ||
|
|
4ae22c4a1e |
@@ -90,6 +90,11 @@ Example::
|
||||
proxy that actively removes and re-adds the header to make sure the correct client IP is the first value.
|
||||
Defaults to ``off``.
|
||||
|
||||
``trust_x_forwarded_proto``
|
||||
Specifies whether the ``X-Forwarded-Proto`` header can be trusted. Only set to ``on`` if you have a reverse
|
||||
proxy that actively removes and re-adds the header to make sure the correct client IP is the first value.
|
||||
Defaults to ``off``.
|
||||
|
||||
|
||||
Locale settings
|
||||
---------------
|
||||
|
||||
@@ -125,6 +125,8 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
; DO NOT change the following value, it has to be set to the location of the
|
||||
; directory *inside* the docker container
|
||||
datadir=/data
|
||||
trust_x_forwarded_for=on
|
||||
trust_x_forwarded_proto=on
|
||||
|
||||
[database]
|
||||
; Replace postgresql with mysql for MySQL
|
||||
|
||||
@@ -85,6 +85,8 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
|
||||
url=https://pretix.mydomain.com
|
||||
currency=EUR
|
||||
datadir=/var/pretix/data
|
||||
trust_x_forwarded_for=on
|
||||
trust_x_forwarded_proto=on
|
||||
|
||||
[database]
|
||||
; For MySQL, replace with "mysql"
|
||||
|
||||
@@ -194,6 +194,7 @@ Cart position endpoints
|
||||
* ``subevent`` (optional)
|
||||
* ``expires`` (optional)
|
||||
* ``includes_tax`` (optional)
|
||||
* ``sales_channel`` (optional)
|
||||
* ``answers``
|
||||
|
||||
* ``question``
|
||||
|
||||
@@ -42,6 +42,7 @@ seating_plan integer If reserved sea
|
||||
plan. Otherwise ``null``.
|
||||
seat_category_mapping object An object mapping categories of the seating plan
|
||||
(strings) to items in the event (integers or ``null``).
|
||||
timezone string Event timezone name
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -74,6 +75,10 @@ seat_category_mapping object An object mappi
|
||||
|
||||
The attributes ``geo_lat`` and ``geo_lon`` have been added.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
|
||||
The attribute ``timezone`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -127,6 +132,7 @@ Endpoints
|
||||
"meta_data": {},
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer"
|
||||
"pretix.plugins.stripe"
|
||||
@@ -197,6 +203,7 @@ Endpoints
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer"
|
||||
"pretix.plugins.stripe"
|
||||
@@ -248,6 +255,7 @@ Endpoints
|
||||
"geo_lon": null,
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
@@ -281,6 +289,7 @@ Endpoints
|
||||
"seat_category_mapping": {},
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
@@ -334,6 +343,7 @@ Endpoints
|
||||
"seat_category_mapping": {},
|
||||
"has_subevents": false,
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
@@ -367,6 +377,7 @@ Endpoints
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
@@ -432,6 +443,7 @@ Endpoints
|
||||
"seating_plan": null,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {},
|
||||
"timezone": "Europe/Berlin",
|
||||
"plugins": [
|
||||
"pretix.plugins.banktransfer",
|
||||
"pretix.plugins.stripe",
|
||||
|
||||
@@ -221,9 +221,14 @@ Endpoints
|
||||
"value": "15.37"
|
||||
}
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
This endpoint now returns status code ``409`` if the transaction would lead to a negative gift card value.
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param id: The ``id`` field of the gift card to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The gift card could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||
:statuscode 409: There is not sufficient credit on the gift card.
|
||||
|
||||
@@ -61,9 +61,10 @@ invoice_address object Invoice address
|
||||
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
||||
EU VAT service and validation was successful. This only
|
||||
happens in rare cases.
|
||||
positions list of objects List of non-canceled order positions (see below)
|
||||
fees list of objects List of non-canceled fees included in the order total
|
||||
(i.e. payment fees)
|
||||
positions list of objects List of order positions (see below). By default, only
|
||||
non-canceled positions are included.
|
||||
fees list of objects List of fees included in the order total. By default, only
|
||||
non-canceled fees are included.
|
||||
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
|
||||
``other``)
|
||||
├ value money (string) Fee amount
|
||||
@@ -72,7 +73,8 @@ fees list of objects List of non-can
|
||||
can be empty
|
||||
├ tax_rate decimal (string) VAT rate applied for this fee
|
||||
├ tax_value money (string) VAT included in this fee
|
||||
└ tax_rule integer The ID of the used tax rule (or ``null``)
|
||||
├ tax_rule integer The ID of the used tax rule (or ``null``)
|
||||
└ canceled boolean Whether or not this fee has been canceled.
|
||||
downloads list of objects List of ticket download options for order-wise ticket
|
||||
downloading. This might be a multi-page PDF or a ZIP
|
||||
file of tickets for outputs that do not support
|
||||
@@ -145,6 +147,10 @@ last_modified datetime Last modificati
|
||||
The ``invoice_address.state`` and ``url`` attributes have been added. When creating orders through the API,
|
||||
vouchers are now supported and many fields are now optional.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``order.fees.canceled`` attribute has been added.
|
||||
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
@@ -159,6 +165,8 @@ Field Type Description
|
||||
id integer Internal ID of the order position
|
||||
order string Order code of the order the position belongs to
|
||||
positionid integer Number of the position within the order
|
||||
canceled boolean Whether or not this position has been canceled. Note that
|
||||
by default, only non-canceled positions are shown.
|
||||
item integer ID of the purchased item
|
||||
variation integer ID of the purchased variation (or ``null``)
|
||||
price money (string) Price of this position
|
||||
@@ -224,6 +232,10 @@ pdf_data object Data object req
|
||||
The ``url`` of a ticket ``download`` can now also return a ``text/uri-list`` instead of a file. See
|
||||
:ref:`order-position-ticket-download` for details.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The attribute ``canceled`` has been added.
|
||||
|
||||
.. _order-payment-resource:
|
||||
|
||||
Order payment resource
|
||||
@@ -290,6 +302,10 @@ List of all orders
|
||||
|
||||
Filtering for emails or order codes is now case-insensitive.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/
|
||||
|
||||
Returns a list of all orders within a given event.
|
||||
@@ -355,6 +371,7 @@ List of all orders
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -427,6 +444,9 @@ List of all orders
|
||||
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
|
||||
:query boolean require_approval: If set to ``true`` or ``false``, only categories with this value for the field
|
||||
``require_approval`` will be returned.
|
||||
:query include_canceled_positions: If set to ``true``, the output will contain canceled order positions. Note that this
|
||||
only affects position-level cancellations, not fully-canceled orders.
|
||||
:query include_canceled_fees: If set to ``true``, the output will contain canceled order fees.
|
||||
:query string email: Only return orders created with the given email address
|
||||
:query string locale: Only return orders with the given customer locale
|
||||
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
|
||||
@@ -444,6 +464,10 @@ List of all orders
|
||||
Fetching individual orders
|
||||
--------------------------
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/
|
||||
|
||||
Returns information on one order, identified by its order code.
|
||||
@@ -503,6 +527,7 @@ Fetching individual orders
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -568,6 +593,9 @@ Fetching individual orders
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param code: The ``code`` field of the order to fetch
|
||||
:query include_canceled_positions: If set to ``true``, the output will contain canceled order positions. Note that this
|
||||
only affects position-level cancellations, not fully-canceled orders.
|
||||
:query include_canceled_fees: If set to ``true``, the output will contain canceled order fees.
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
@@ -1313,8 +1341,9 @@ List of all order positions
|
||||
|
||||
The value ``auto_checked_in`` has been added to the ``checkins``-attribute.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
.. note:: Individually canceled order positions are currently not visible via the API at all.
|
||||
The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/
|
||||
|
||||
@@ -1345,6 +1374,7 @@ List of all order positions
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -1414,6 +1444,8 @@ List of all order positions
|
||||
comma-separated IDs.
|
||||
:query string voucher: Only return positions with a specific voucher.
|
||||
:query string voucher__code: Only return positions with a specific voucher code.
|
||||
:query include_canceled_positions: If set to ``true``, the output will contain canceled order positions. Note that this
|
||||
only affects position-level cancellations, not fully-canceled orders.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
@@ -1447,6 +1479,7 @@ Fetching individual positions
|
||||
"id": 23442,
|
||||
"order": "ABC12",
|
||||
"positionid": 1,
|
||||
"canceled": false,
|
||||
"item": 1345,
|
||||
"variation": null,
|
||||
"price": "23.00",
|
||||
@@ -1491,6 +1524,7 @@ Fetching individual positions
|
||||
: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 order position to fetch
|
||||
:query include_canceled_positions: If set to ``true``, canceled positions may be returned (otherwise, they return 404).
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
@@ -18,6 +18,7 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the question
|
||||
question multi-lingual string The field label shown to the customer
|
||||
help_text multi-lingual string The help text shown to the customer
|
||||
type string The expected type of answer. Valid options:
|
||||
|
||||
* ``N`` – number
|
||||
@@ -87,6 +88,10 @@ dependency_value string An old version
|
||||
|
||||
The attribute ``print_on_invoice`` has been added.
|
||||
|
||||
.. versionchanged:: 3.5
|
||||
|
||||
The attribute ``help_text`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -123,6 +128,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -193,6 +199,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -248,6 +255,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -282,6 +290,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
@@ -356,6 +365,7 @@ Endpoints
|
||||
{
|
||||
"id": 1,
|
||||
"question": {"en": "T-Shirt size"},
|
||||
"help_text": {"en": "Choose your preferred t-shirt-size"},
|
||||
"type": "C",
|
||||
"required": false,
|
||||
"items": [1, 2],
|
||||
|
||||
@@ -26,7 +26,7 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, item_description
|
||||
:members: html_head, html_footer, footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, item_description
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
|
||||
112
doc/development/api/import.rst
Normal file
112
doc/development/api/import.rst
Normal file
@@ -0,0 +1,112 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
.. _`importcol`:
|
||||
|
||||
Extending the order import process
|
||||
==================================
|
||||
|
||||
It's possible through the backend to import orders into pretix, for example from a legacy ticketing system. If your
|
||||
plugins defines additional data structures around orders, it might be useful to make it possible to import them as well.
|
||||
|
||||
Import process
|
||||
--------------
|
||||
|
||||
Here's a short description of pretix' import process to show you where the system will need to interact with your plugin.
|
||||
You can find more detailed descriptions of the attributes and methods further below.
|
||||
|
||||
1. The user uploads a CSV file. The system tries to parse the CSV file and understand its column headers.
|
||||
|
||||
2. A preview of the file is shown to the user and the user is asked to assign the various different input parameters to
|
||||
columns of the file or static values. For example, the user either needs to manually select a product or specify a
|
||||
column that contains a product. For this purpose, a select field is rendered for every possible input column,
|
||||
allowing the user to choose between a default/empty value (defined by your ``default_value``/``default_label``)
|
||||
attributes, the columns of the uploaded file, or a static value (defined by your ``static_choices`` method).
|
||||
|
||||
3. The user submits its assignment and the system uses the ``resolve`` method of all columns to get the raw value for
|
||||
all columns.
|
||||
|
||||
4. The system uses the ``clean`` method of all columns to verify that all input fields are valid and transformed to the
|
||||
correct data type.
|
||||
|
||||
5. The system prepares internal model objects (``Order`` etc) and uses the ``assign`` method of all columns to assign
|
||||
these objects with actual values.
|
||||
|
||||
6. The system saves all of these model objects to the database in a database transaction. Plugins can create additional
|
||||
objects in this stage through their ``save`` method.
|
||||
|
||||
Column registration
|
||||
-------------------
|
||||
|
||||
The import API does not make a lot of usage from signals, however, it
|
||||
does use a signal to get a list of all available import columns. Your plugin
|
||||
should listen for this signal and return the subclass of ``pretix.base.orderimport.ImportColumn``
|
||||
that we'll provide in this plugin:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.signals import order_import_columns
|
||||
|
||||
|
||||
@receiver(order_import_columns, dispatch_uid="custom_columns")
|
||||
def register_column(sender, **kwargs):
|
||||
return [
|
||||
EmailColumn(sender),
|
||||
]
|
||||
|
||||
The column class API
|
||||
--------------------
|
||||
|
||||
.. class:: pretix.base.orderimport.ImportColumn
|
||||
|
||||
The central object of each import extension is the subclass of ``ImportColumn``.
|
||||
|
||||
.. py:attribute:: ImportColumn.event
|
||||
|
||||
The default constructor sets this property to the event we are currently
|
||||
working for.
|
||||
|
||||
.. autoattribute:: identifier
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: verbose_name
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: default_value
|
||||
|
||||
.. autoattribute:: default_label
|
||||
|
||||
.. autoattribute:: initial
|
||||
|
||||
.. automethod:: static_choices
|
||||
|
||||
.. automethod:: resolve
|
||||
|
||||
.. automethod:: clean
|
||||
|
||||
.. automethod:: assign
|
||||
|
||||
.. automethod:: save
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
For example, the import column responsible for assigning email addresses looks like this:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class EmailColumn(ImportColumn):
|
||||
identifier = 'email'
|
||||
verbose_name = _('E-mail address')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
EmailValidator()(value)
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
order.email = value
|
||||
@@ -15,6 +15,7 @@ Contents:
|
||||
placeholder
|
||||
invoice
|
||||
shredder
|
||||
import
|
||||
customview
|
||||
auth
|
||||
general
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.4.0"
|
||||
__version__ = "3.5.0"
|
||||
|
||||
@@ -30,11 +30,12 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
expires = serializers.DateTimeField(required=False)
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
sales_channel = serializers.CharField(required=False, default='sales_channel')
|
||||
|
||||
class Meta:
|
||||
model = CartPosition
|
||||
fields = ('cart_id', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||
'subevent', 'expires', 'includes_tax', 'answers', 'seat')
|
||||
'subevent', 'expires', 'includes_tax', 'answers', 'seat', 'sales_channel')
|
||||
|
||||
def create(self, validated_data):
|
||||
answers_data = validated_data.pop('answers')
|
||||
@@ -86,11 +87,12 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError('The specified seat ID is not unique.')
|
||||
else:
|
||||
validated_data['seat'] = seat
|
||||
if not seat.is_available():
|
||||
if not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web')):
|
||||
raise ValidationError(ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name))
|
||||
elif seated:
|
||||
raise ValidationError('The specified product requires to choose a seat.')
|
||||
|
||||
validated_data.pop('sales_channel')
|
||||
cp = CartPosition.objects.create(event=self.context['event'], **validated_data)
|
||||
|
||||
for answ_data in answers_data:
|
||||
|
||||
@@ -4,7 +4,8 @@ from django.db import transaction
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext as _
|
||||
from django_countries.serializers import CountryFieldMixin
|
||||
from rest_framework.fields import Field
|
||||
from pytz import common_timezones
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
@@ -61,17 +62,27 @@ class PluginsField(Field):
|
||||
}
|
||||
|
||||
|
||||
class TimeZoneField(ChoiceField):
|
||||
def get_attribute(self, instance):
|
||||
return instance.cache.get_or_set(
|
||||
'timezone_name',
|
||||
lambda: instance.settings.timezone,
|
||||
3600
|
||||
)
|
||||
|
||||
|
||||
class EventSerializer(I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
|
||||
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping')
|
||||
'plugins', 'seat_category_mapping', 'timezone')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -156,8 +167,12 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
validated_data.pop('seat_category_mapping', None)
|
||||
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
|
||||
tz = validated_data.pop('timezone', None)
|
||||
event = super().create(validated_data)
|
||||
|
||||
if tz:
|
||||
event.settings.timezone = tz
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
@@ -182,8 +197,12 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
plugins = validated_data.pop('plugins', None)
|
||||
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
|
||||
tz = validated_data.pop('timezone', None)
|
||||
event = super().update(instance, validated_data)
|
||||
|
||||
if tz:
|
||||
event.settings.timezone = tz
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
current = {mv.property: mv for mv in event.meta_values.select_related('property')}
|
||||
@@ -240,6 +259,7 @@ class CloneEventSerializer(EventSerializer):
|
||||
is_public = validated_data.pop('is_public', None)
|
||||
testmode = validated_data.pop('testmode', None)
|
||||
has_subevents = validated_data.pop('has_subevents', None)
|
||||
tz = validated_data.pop('timezone', None)
|
||||
new_event = super().create(validated_data)
|
||||
|
||||
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
||||
@@ -254,6 +274,8 @@ class CloneEventSerializer(EventSerializer):
|
||||
if has_subevents is not None:
|
||||
new_event.has_subevents = has_subevents
|
||||
new_event.save()
|
||||
if tz:
|
||||
new_event.settings.timezone = tz
|
||||
|
||||
return new_event
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
model = Question
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'hidden', 'dependency_value', 'print_on_invoice')
|
||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text')
|
||||
|
||||
def validate_identifier(self, value):
|
||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||
|
||||
@@ -204,7 +204,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat')
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -284,7 +284,7 @@ class OrderPaymentDateField(serializers.DateField):
|
||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled')
|
||||
|
||||
|
||||
class PaymentURLField(serializers.URLField):
|
||||
@@ -720,6 +720,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
consume_carts = validated_data.pop('consume_carts', [])
|
||||
delete_cps = []
|
||||
quota_avail_cache = {}
|
||||
v_budget = {}
|
||||
voucher_usage = Counter()
|
||||
if consume_carts:
|
||||
for cp in CartPosition.objects.filter(
|
||||
@@ -742,9 +743,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
errs = [{} for p in positions_data]
|
||||
|
||||
for i, pos_data in enumerate(positions_data):
|
||||
|
||||
if pos_data.get('voucher'):
|
||||
v = pos_data['voucher']
|
||||
|
||||
if pos_data.get('addon_to'):
|
||||
errs[i]['voucher'] = ['Vouchers are currently not supported for add-on products.']
|
||||
continue
|
||||
|
||||
if not v.applies_to(pos_data['item'], pos_data.get('variation')):
|
||||
errs[i]['voucher'] = [error_messages['voucher_invalid_item']]
|
||||
continue
|
||||
@@ -768,6 +774,44 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
'The voucher has already been used the maximum number of times.'
|
||||
]
|
||||
|
||||
if v.budget is not None:
|
||||
price = pos_data.get('price')
|
||||
if price is None:
|
||||
price = get_price(
|
||||
item=pos_data.get('item'),
|
||||
variation=pos_data.get('variation'),
|
||||
voucher=v,
|
||||
custom_price=None,
|
||||
subevent=pos_data.get('subevent'),
|
||||
addon_to=pos_data.get('addon_to'),
|
||||
invoice_address=ia,
|
||||
).gross
|
||||
pbv = get_price(
|
||||
item=pos_data['item'],
|
||||
variation=pos_data.get('variation'),
|
||||
voucher=None,
|
||||
custom_price=None,
|
||||
subevent=pos_data.get('subevent'),
|
||||
addon_to=pos_data.get('addon_to'),
|
||||
invoice_address=ia,
|
||||
)
|
||||
|
||||
if v not in v_budget:
|
||||
v_budget[v] = v.budget - v.budget_used()
|
||||
disc = pbv.gross - price
|
||||
if disc > v_budget[v]:
|
||||
new_disc = v_budget[v]
|
||||
v_budget[v] -= new_disc
|
||||
if new_disc == Decimal('0.00') or pos_data.get('price') is not None:
|
||||
errs[i]['voucher'] = [
|
||||
'The voucher has a remaining budget of {}, therefore a discount of {} can not be '
|
||||
'given.'.format(v_budget[v] + new_disc, disc)
|
||||
]
|
||||
continue
|
||||
pos_data['price'] = price + (disc - new_disc)
|
||||
else:
|
||||
v_budget[v] -= disc
|
||||
|
||||
seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists()
|
||||
if pos_data.get('seat'):
|
||||
if not seated:
|
||||
@@ -778,7 +822,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||
else:
|
||||
pos_data['seat'] = seat
|
||||
if (seat not in free_seats and not seat.is_available()) or seat in seats_seen:
|
||||
if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen:
|
||||
errs[i]['seat'] = [ugettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
seats_seen.add(seat)
|
||||
elif seated:
|
||||
@@ -856,6 +900,17 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
pos.tax_rule = pos.item.tax_rule
|
||||
else:
|
||||
pos._calculate_tax()
|
||||
|
||||
pos.price_before_voucher = get_price(
|
||||
item=pos.item,
|
||||
variation=pos.variation,
|
||||
voucher=None,
|
||||
custom_price=None,
|
||||
subevent=pos.subevent,
|
||||
addon_to=pos.addon_to,
|
||||
invoice_address=ia,
|
||||
).gross
|
||||
|
||||
if pos.voucher:
|
||||
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
|
||||
pos.save()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
@@ -26,7 +28,7 @@ class SeatingPlanSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
value = serializers.DecimalField(max_digits=10, decimal_places=2)
|
||||
value = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=Decimal('0.00'))
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -133,6 +133,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(organizer=self.request.organizer)
|
||||
serializer.instance.set_defaults()
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.added',
|
||||
user=self.request.user,
|
||||
|
||||
@@ -30,8 +30,8 @@ from pretix.api.serializers.order import (
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress,
|
||||
Order, OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken,
|
||||
generate_position_secret, generate_secret,
|
||||
Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota,
|
||||
TeamAPIToken, generate_position_secret, generate_secret,
|
||||
)
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services import tickets
|
||||
@@ -82,20 +82,29 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
return ctx
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.query_params.get('include_canceled_fees', 'false') == 'true':
|
||||
fqs = OrderFee.all
|
||||
else:
|
||||
fqs = OrderFee.objects
|
||||
qs = self.request.event.orders.prefetch_related(
|
||||
'fees', 'payments', 'refunds', 'refunds__payment'
|
||||
Prefetch('fees', queryset=fqs.all()),
|
||||
'payments', 'refunds', 'refunds__payment'
|
||||
).select_related(
|
||||
'invoice_address'
|
||||
)
|
||||
|
||||
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
|
||||
opq = OrderPosition.all
|
||||
else:
|
||||
opq = OrderPosition.objects
|
||||
if self.request.query_params.get('pdf_data', 'false') == 'true':
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.all().prefetch_related(
|
||||
opq.all().prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
'item__category', 'addon_to', 'seat',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation', 'seat'))
|
||||
Prefetch('addons', opq.select_related('item', 'variation', 'seat'))
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -103,7 +112,7 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.all().prefetch_related(
|
||||
opq.all().prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat',
|
||||
)
|
||||
)
|
||||
@@ -654,11 +663,16 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
qs = OrderPosition.objects.filter(order__event=self.request.event)
|
||||
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
|
||||
qs = OrderPosition.all
|
||||
else:
|
||||
qs = OrderPosition.objects
|
||||
|
||||
qs = qs.filter(order__event=self.request.event)
|
||||
if self.request.query_params.get('pdf_data', 'false') == 'true':
|
||||
qs = qs.prefetch_related(
|
||||
'checkins', 'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||
Prefetch('addons', qs.select_related('item', 'variation')),
|
||||
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||
Prefetch(
|
||||
'event',
|
||||
@@ -666,7 +680,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
),
|
||||
Prefetch(
|
||||
'positions',
|
||||
OrderPosition.objects.prefetch_related(
|
||||
qs.prefetch_related(
|
||||
'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
|
||||
)
|
||||
)
|
||||
@@ -676,7 +690,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
|
||||
)
|
||||
else:
|
||||
qs = qs.prefetch_related(
|
||||
'checkins', 'answers', 'answers__options', 'answers__question'
|
||||
'checkins', 'answers', 'answers__options', 'answers__question',
|
||||
).select_related(
|
||||
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import transaction
|
||||
from rest_framework import filters, serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
@@ -136,6 +138,10 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
|
||||
request.data.get('value')
|
||||
)
|
||||
if gc.value + value < Decimal('0.00'):
|
||||
return Response({
|
||||
'value': ['The gift card does not have sufficient credit for this operation.']
|
||||
}, status=status.HTTP_409_CONFLICT)
|
||||
gc.transactions.create(value=value)
|
||||
gc.log_action(
|
||||
'pretix.giftcards.transaction.manual',
|
||||
|
||||
@@ -13,7 +13,7 @@ class PretixBaseConfig(AppConfig):
|
||||
from . import invoice # NOQA
|
||||
from . import notifications # NOQA
|
||||
from . import email # NOQA
|
||||
from .services import auth, checkin, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
||||
from .services import auth, checkin, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
||||
|
||||
try:
|
||||
from .celery_app import app as celery_app # NOQA
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
from inlinestyler.utils import inline_css
|
||||
|
||||
from pretix.base.i18n import LazyCurrencyNumber, LazyDate, LazyNumber
|
||||
@@ -112,7 +112,8 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
'site_url': settings.SITE_URL,
|
||||
'body': body_md,
|
||||
'subject': str(subject),
|
||||
'color': '#8E44B3'
|
||||
'color': '#8E44B3',
|
||||
'rtl': get_language() in settings.LANGUAGES_RTL
|
||||
}
|
||||
if self.event:
|
||||
htmlctx['event'] = self.event
|
||||
|
||||
@@ -128,7 +128,7 @@ class ListExporter(BaseExporter):
|
||||
|
||||
def _render_xlsx(self, form_data, output_file=None):
|
||||
wb = Workbook()
|
||||
ws = wb.get_active_sheet()
|
||||
ws = wb.active
|
||||
try:
|
||||
ws.title = str(self.verbose_name)
|
||||
except:
|
||||
@@ -207,7 +207,7 @@ class MultiSheetListExporter(ListExporter):
|
||||
|
||||
def _render_xlsx(self, form_data, output_file=None):
|
||||
wb = Workbook()
|
||||
ws = wb.get_active_sheet()
|
||||
ws = wb.active
|
||||
wb.remove(ws)
|
||||
for s, l in self.sheets:
|
||||
ws = wb.create_sheet(str(l))
|
||||
|
||||
@@ -32,7 +32,8 @@ class LoginForm(forms.Form):
|
||||
for k, f in backend.login_form_fields.items():
|
||||
self.fields[k] = f
|
||||
|
||||
if not settings.PRETIX_LONG_SESSIONS:
|
||||
# Authentication backends which use urls cannot have long sessions.
|
||||
if not settings.PRETIX_LONG_SESSIONS or backend.url:
|
||||
del self.fields['keep_logged_in']
|
||||
else:
|
||||
self.fields.move_to_end('keep_logged_in')
|
||||
@@ -197,6 +198,7 @@ class ReauthForm(forms.Form):
|
||||
self.request = request
|
||||
self.user = user
|
||||
self.backend = backend
|
||||
self.backend.url = backend.authentication_url(self.request)
|
||||
super().__init__(*args, **kwargs)
|
||||
for k, f in backend.login_form_fields.items():
|
||||
self.fields[k] = f
|
||||
|
||||
@@ -490,10 +490,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
story = [
|
||||
NextPageTemplate('FirstPage'),
|
||||
Paragraph(pgettext('invoice', 'Invoice')
|
||||
if not self.invoice.is_cancellation
|
||||
else pgettext('invoice', 'Cancellation'),
|
||||
self.stylesheet['Heading1']),
|
||||
Paragraph(
|
||||
(
|
||||
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
||||
else pgettext('invoice', 'Invoice')
|
||||
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
|
||||
self.stylesheet['Heading1']
|
||||
),
|
||||
Spacer(1, 5 * mm),
|
||||
NextPageTemplate('OtherPages'),
|
||||
]
|
||||
|
||||
30
src/pretix/base/migrations/0142_auto_20191215_1522.py
Normal file
30
src/pretix/base/migrations/0142_auto_20191215_1522.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 2.2.7 on 2019-12-15 15:22
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0141_seat_sorting_rank'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='price_before_voucher',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='price_before_voucher',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='budget',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
|
||||
),
|
||||
]
|
||||
@@ -363,6 +363,14 @@ class Event(EventMixin, LoggedModel):
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def set_defaults(self):
|
||||
"""
|
||||
This will be called after event creation, but only if the event was not created by copying an existing one.
|
||||
This way, we can use this to introduce new default settings to pretix that do not affect existing events.
|
||||
"""
|
||||
self.settings.invoice_renderer = 'modern1'
|
||||
self.settings.invoice_include_expire_date = True
|
||||
|
||||
@property
|
||||
def social_image(self):
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
@@ -377,7 +385,7 @@ class Event(EventMixin, LoggedModel):
|
||||
if img:
|
||||
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
|
||||
|
||||
def free_seats(self, ignore_voucher=None):
|
||||
def free_seats(self, ignore_voucher=None, sales_channel='web'):
|
||||
from .orders import CartPosition, Order, OrderPosition
|
||||
from .vouchers import Voucher
|
||||
vqs = Voucher.objects.filter(
|
||||
@@ -389,7 +397,7 @@ class Event(EventMixin, LoggedModel):
|
||||
)
|
||||
if ignore_voucher:
|
||||
vqs = vqs.exclude(pk=ignore_voucher.pk)
|
||||
return self.seats.annotate(
|
||||
qs = self.seats.annotate(
|
||||
has_order=Exists(
|
||||
OrderPosition.objects.filter(
|
||||
order__event=self,
|
||||
@@ -407,7 +415,10 @@ class Event(EventMixin, LoggedModel):
|
||||
has_voucher=Exists(
|
||||
vqs
|
||||
)
|
||||
).filter(has_order=False, has_cart=False, has_voucher=False, blocked=False)
|
||||
).filter(has_order=False, has_cart=False, has_voucher=False)
|
||||
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel:
|
||||
qs = qs.filter(blocked=False)
|
||||
return qs
|
||||
|
||||
@property
|
||||
def presale_has_ended(self):
|
||||
@@ -511,6 +522,7 @@ class Event(EventMixin, LoggedModel):
|
||||
self.is_public = other.is_public
|
||||
self.testmode = other.testmode
|
||||
self.save()
|
||||
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
||||
|
||||
tax_map = {}
|
||||
for t in other.tax_rules.all():
|
||||
@@ -518,6 +530,7 @@ class Event(EventMixin, LoggedModel):
|
||||
t.pk = None
|
||||
t.event = self
|
||||
t.save()
|
||||
t.log_action('pretix.object.cloned')
|
||||
|
||||
category_map = {}
|
||||
for c in ItemCategory.objects.filter(event=other):
|
||||
@@ -525,6 +538,7 @@ class Event(EventMixin, LoggedModel):
|
||||
c.pk = None
|
||||
c.event = self
|
||||
c.save()
|
||||
c.log_action('pretix.object.cloned')
|
||||
|
||||
item_map = {}
|
||||
variation_map = {}
|
||||
@@ -540,6 +554,7 @@ class Event(EventMixin, LoggedModel):
|
||||
if i.tax_rule_id:
|
||||
i.tax_rule = tax_map[i.tax_rule_id]
|
||||
i.save()
|
||||
i.log_action('pretix.object.cloned')
|
||||
for v in vars:
|
||||
variation_map[v.pk] = v
|
||||
v.pk = None
|
||||
@@ -564,6 +579,7 @@ class Event(EventMixin, LoggedModel):
|
||||
q.cached_availability_time = None
|
||||
q.closed = False
|
||||
q.save()
|
||||
q.log_action('pretix.object.cloned')
|
||||
for i in items:
|
||||
if i.pk in item_map:
|
||||
q.items.add(item_map[i.pk])
|
||||
@@ -579,6 +595,7 @@ class Event(EventMixin, LoggedModel):
|
||||
q.pk = None
|
||||
q.event = self
|
||||
q.save()
|
||||
q.log_action('pretix.object.cloned')
|
||||
|
||||
for i in items:
|
||||
q.items.add(item_map[i.pk])
|
||||
@@ -596,6 +613,7 @@ class Event(EventMixin, LoggedModel):
|
||||
cl.pk = None
|
||||
cl.event = self
|
||||
cl.save()
|
||||
cl.log_action('pretix.object.cloned')
|
||||
for i in items:
|
||||
cl.limit_products.add(item_map[i.pk])
|
||||
|
||||
@@ -998,7 +1016,7 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
def __str__(self):
|
||||
return '{} - {}'.format(self.name, self.get_date_range_display())
|
||||
|
||||
def free_seats(self, ignore_voucher=None):
|
||||
def free_seats(self, ignore_voucher=None, sales_channel='web'):
|
||||
from .orders import CartPosition, Order, OrderPosition
|
||||
from .vouchers import Voucher
|
||||
vqs = Voucher.objects.filter(
|
||||
@@ -1011,7 +1029,7 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
)
|
||||
if ignore_voucher:
|
||||
vqs = vqs.exclude(pk=ignore_voucher.pk)
|
||||
return self.seats.annotate(
|
||||
qs = self.seats.annotate(
|
||||
has_order=Exists(
|
||||
OrderPosition.objects.filter(
|
||||
order__event_id=self.event_id,
|
||||
@@ -1031,7 +1049,10 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
has_voucher=Exists(
|
||||
vqs
|
||||
)
|
||||
).filter(has_order=False, has_cart=False, blocked=False, has_voucher=False)
|
||||
).filter(has_order=False, has_cart=False, has_voucher=False)
|
||||
if sales_channel not in self.settings.seating_allow_blocked_seats_for_channel:
|
||||
qs = qs.filter(blocked=False)
|
||||
return qs
|
||||
|
||||
@cached_property
|
||||
def settings(self):
|
||||
|
||||
@@ -10,10 +10,10 @@ from pretix.base.banlist import banned
|
||||
from pretix.base.models import LoggedModel
|
||||
|
||||
|
||||
def gen_giftcard_secret():
|
||||
def gen_giftcard_secret(length):
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
|
||||
while True:
|
||||
code = get_random_string(length=settings.ENTROPY['giftcard_secret'], allowed_chars=charset)
|
||||
code = get_random_string(length=length, allowed_chars=charset)
|
||||
if not banned(code) and not GiftCard.objects.filter(secret=code).exists():
|
||||
return code
|
||||
|
||||
@@ -48,7 +48,6 @@ class GiftCard(LoggedModel):
|
||||
)
|
||||
secret = models.CharField(
|
||||
max_length=190,
|
||||
default=gen_giftcard_secret,
|
||||
db_index=True,
|
||||
verbose_name=_('Gift card code'),
|
||||
)
|
||||
@@ -69,6 +68,12 @@ class GiftCard(LoggedModel):
|
||||
def accepted_by(self, organizer):
|
||||
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, collector=organizer).exists()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.secret:
|
||||
self.secret = gen_giftcard_secret(self.issuer.settings.giftcard_length)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('secret', 'issuer'),)
|
||||
|
||||
|
||||
@@ -191,6 +191,9 @@ class Invoice(models.Model):
|
||||
self.prefix = self.event.settings.invoice_numbers_prefix or (self.event.slug.upper() + '-')
|
||||
if self.is_cancellation:
|
||||
self.prefix = self.event.settings.invoice_numbers_prefix_cancellations or self.prefix
|
||||
if '%' in self.prefix:
|
||||
self.prefix = self.date.strftime(self.prefix)
|
||||
|
||||
if not self.invoice_no:
|
||||
if self.order.testmode:
|
||||
self.prefix += 'TEST-'
|
||||
|
||||
@@ -1106,17 +1106,25 @@ class Question(LoggedModel):
|
||||
|
||||
if self.type == Question.TYPE_CHOICE:
|
||||
try:
|
||||
return self.options.get(pk=answer)
|
||||
return self.options.get(Q(pk=answer) | Q(identifier=answer))
|
||||
except:
|
||||
raise ValidationError(_('Invalid option selected.'))
|
||||
elif self.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
try:
|
||||
if isinstance(answer, str):
|
||||
return list(self.options.filter(pk__in=answer.split(",")))
|
||||
else:
|
||||
return list(self.options.filter(pk__in=answer))
|
||||
except:
|
||||
if isinstance(answer, str):
|
||||
l_ = list(self.options.filter(
|
||||
Q(pk__in=[a for a in answer.split(",") if a.isdigit()]) |
|
||||
Q(identifier__in=answer.split(","))
|
||||
))
|
||||
llen = len(answer.split(','))
|
||||
else:
|
||||
l_ = list(self.options.filter(
|
||||
Q(pk__in=[a for a in answer if isinstance(a, int) or a.isdigit()]) |
|
||||
Q(identifier__in=answer)
|
||||
))
|
||||
llen = len(answer)
|
||||
if len(l_) != llen:
|
||||
raise ValidationError(_('Invalid option selected.'))
|
||||
return l_
|
||||
elif self.type == Question.TYPE_BOOLEAN:
|
||||
return answer in ('true', 'True', True)
|
||||
elif self.type == Question.TYPE_NUMBER:
|
||||
|
||||
@@ -687,10 +687,12 @@ class Order(LockModel, LoggedModel):
|
||||
error_messages = {
|
||||
'unavailable': _('The ordered product "{item}" is no longer available.'),
|
||||
'seat_unavailable': _('The seat "{seat}" is no longer available.'),
|
||||
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
|
||||
}
|
||||
now_dt = now_dt or now()
|
||||
positions = self.positions.all().select_related('item', 'variation', 'seat')
|
||||
positions = self.positions.all().select_related('item', 'variation', 'seat', 'voucher')
|
||||
quota_cache = {}
|
||||
v_budget = {}
|
||||
try:
|
||||
for i, op in enumerate(positions):
|
||||
if op.seat:
|
||||
@@ -699,6 +701,16 @@ class Order(LockModel, LoggedModel):
|
||||
if force:
|
||||
continue
|
||||
|
||||
if op.voucher and op.voucher.budget is not None and op.price_before_voucher is not None:
|
||||
if op.voucher not in v_budget:
|
||||
v_budget[op.voucher] = op.voucher.budget - op.voucher.budget_used()
|
||||
disc = op.price_before_voucher - op.price
|
||||
if disc > v_budget[op.voucher]:
|
||||
raise Quota.QuotaExceededException(error_messages['voucher_budget'].format(
|
||||
voucher=op.voucher.code
|
||||
))
|
||||
v_budget[op.voucher] -= disc
|
||||
|
||||
quotas = list(op.quotas)
|
||||
if len(quotas) == 0:
|
||||
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
|
||||
@@ -991,6 +1003,9 @@ class AbstractPosition(models.Model):
|
||||
verbose_name=_("Variation"),
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
price_before_voucher = models.DecimalField(
|
||||
decimal_places=2, max_digits=10, null=True,
|
||||
)
|
||||
price = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
verbose_name=_("Price")
|
||||
@@ -2033,7 +2048,7 @@ class InvoiceAddress(models.Model):
|
||||
internal_reference = models.TextField(
|
||||
verbose_name=_('Internal reference'),
|
||||
help_text=_('This reference will be printed on your invoice for your convenience.'),
|
||||
blank=True
|
||||
blank=True,
|
||||
)
|
||||
beneficiary = models.TextField(
|
||||
verbose_name=_('Beneficiary'),
|
||||
@@ -2053,6 +2068,13 @@ class InvoiceAddress(models.Model):
|
||||
self.name_parts = {}
|
||||
super().save(**kwargs)
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
return (
|
||||
not self.name_cached and not self.company and not self.street and not self.zipcode and not self.city
|
||||
and not self.internal_reference and not self.beneficiary
|
||||
)
|
||||
|
||||
@property
|
||||
def state_name(self):
|
||||
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
|
||||
|
||||
@@ -135,12 +135,15 @@ class Seat(models.Model):
|
||||
return self.name
|
||||
return ', '.join(parts)
|
||||
|
||||
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None):
|
||||
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None, sales_channel='web'):
|
||||
from .orders import Order
|
||||
|
||||
if self.blocked:
|
||||
if self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
|
||||
return False
|
||||
opqs = self.orderposition_set.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
|
||||
opqs = self.orderposition_set.filter(
|
||||
order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID],
|
||||
canceled=False
|
||||
)
|
||||
cpqs = self.cartposition_set.filter(expires__gte=now())
|
||||
vqs = self.vouchers.filter(
|
||||
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now())) &
|
||||
|
||||
@@ -4,7 +4,8 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models import F, OuterRef, Q, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
@@ -17,7 +18,7 @@ from ..decimal import round_decimal
|
||||
from .base import LoggedModel
|
||||
from .event import Event, SubEvent
|
||||
from .items import Item, ItemVariation, Quota
|
||||
from .orders import Order
|
||||
from .orders import Order, OrderPosition
|
||||
|
||||
|
||||
def _generate_random_code(prefix=None):
|
||||
@@ -114,6 +115,13 @@ class Voucher(LoggedModel):
|
||||
verbose_name=_("Redeemed"),
|
||||
default=0
|
||||
)
|
||||
budget = models.DecimalField(
|
||||
verbose_name=_("Maximum discount budget"),
|
||||
help_text=_("This is the maximum monetary amount that will be discounted using this voucher across all usages. "
|
||||
"If this is sum reached, the voucher can no longer be used."),
|
||||
decimal_places=2, max_digits=10,
|
||||
null=True, blank=True
|
||||
)
|
||||
valid_until = models.DateTimeField(
|
||||
blank=True, null=True, db_index=True,
|
||||
verbose_name=_("Valid until")
|
||||
@@ -430,7 +438,7 @@ class Voucher(LoggedModel):
|
||||
return False
|
||||
return True
|
||||
|
||||
def calculate_price(self, original_price: Decimal) -> Decimal:
|
||||
def calculate_price(self, original_price: Decimal, max_discount: Decimal=None) -> Decimal:
|
||||
"""
|
||||
Returns how the price given in original_price would be modified if this
|
||||
voucher is applied, i.e. replaced by a different price or reduced by a
|
||||
@@ -448,7 +456,9 @@ class Voucher(LoggedModel):
|
||||
p = original_price
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
if places < 2:
|
||||
return p.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
|
||||
p = p.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
|
||||
if max_discount is not None:
|
||||
p = max(p, original_price - max_discount)
|
||||
return p
|
||||
return original_price
|
||||
|
||||
@@ -460,7 +470,7 @@ class Voucher(LoggedModel):
|
||||
|
||||
return Order.objects.filter(all_positions__voucher__in=[self]).distinct()
|
||||
|
||||
def seating_available(self):
|
||||
def seating_available(self, subevent):
|
||||
kwargs = {}
|
||||
if self.subevent:
|
||||
kwargs['subevent'] = self.subevent
|
||||
@@ -469,4 +479,27 @@ class Voucher(LoggedModel):
|
||||
elif self.item_id:
|
||||
return self.item.seat_category_mappings.filter(**kwargs).exists()
|
||||
else:
|
||||
return False
|
||||
return bool(subevent.seating_plan) if subevent else self.event.seating_plan
|
||||
|
||||
@classmethod
|
||||
def annotate_budget_used_orders(cls, qs):
|
||||
opq = OrderPosition.objects.filter(
|
||||
voucher_id=OuterRef('pk'),
|
||||
price_before_voucher__isnull=False,
|
||||
order__status__in=[
|
||||
Order.STATUS_PAID,
|
||||
Order.STATUS_PENDING
|
||||
]
|
||||
).order_by().values('voucher_id').annotate(s=Sum(F('price_before_voucher') - F('price'))).values('s')
|
||||
return qs.annotate(budget_used_orders=Coalesce(Subquery(opq, output_field=models.DecimalField(max_digits=10, decimal_places=2)), Decimal('0.00')))
|
||||
|
||||
def budget_used(self):
|
||||
ops = OrderPosition.objects.filter(
|
||||
voucher=self,
|
||||
price_before_voucher__isnull=False,
|
||||
order__status__in=[
|
||||
Order.STATUS_PAID,
|
||||
Order.STATUS_PENDING
|
||||
]
|
||||
).aggregate(s=Sum(F('price_before_voucher') - F('price')))['s'] or Decimal('0.00')
|
||||
return ops
|
||||
|
||||
612
src/pretix/base/orderimport.py
Normal file
612
src/pretix/base/orderimport.py
Normal file
@@ -0,0 +1,612 @@
|
||||
import re
|
||||
from decimal import Decimal, DecimalException
|
||||
|
||||
import pycountry
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import EmailValidator
|
||||
from django.utils import formats
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import (
|
||||
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
|
||||
)
|
||||
from django_countries import countries
|
||||
from django_countries.fields import Country
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.forms.questions import guess_country
|
||||
from pretix.base.models import (
|
||||
ItemVariation, OrderPosition, QuestionAnswer, QuestionOption, Seat,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.settings import (
|
||||
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
|
||||
)
|
||||
from pretix.base.signals import order_import_columns
|
||||
|
||||
|
||||
class ImportColumn:
|
||||
@property
|
||||
def identifier(self):
|
||||
"""
|
||||
Unique, internal name of the column.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
"""
|
||||
Human-readable description of the column
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def initial(self):
|
||||
"""
|
||||
Initial value for the form component
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def default_value(self):
|
||||
"""
|
||||
Internal default value for the assignment of this column. Defaults to ``empty``. Return ``None`` to disable this
|
||||
option.
|
||||
"""
|
||||
return 'empty'
|
||||
|
||||
@property
|
||||
def default_label(self):
|
||||
"""
|
||||
Human-readable description of the default assignment of this column, defaults to "Keep empty".
|
||||
"""
|
||||
return gettext_lazy('Keep empty')
|
||||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
|
||||
def static_choices(self):
|
||||
"""
|
||||
This will be called when rendering the form component and allows you to return a list of values that can be
|
||||
selected by the user statically during import.
|
||||
|
||||
:return: list of 2-tuples of strings
|
||||
"""
|
||||
return []
|
||||
|
||||
def resolve(self, settings, record):
|
||||
"""
|
||||
This method will be called to get the raw value for this field, usually by either using a static value or
|
||||
inspecting the CSV file for the assigned header. You usually do not need to implement this on your own,
|
||||
the default should be fine.
|
||||
"""
|
||||
k = settings.get(self.identifier, self.default_value)
|
||||
if k == self.default_value:
|
||||
return None
|
||||
elif k.startswith('csv:'):
|
||||
return record.get(k[4:], None) or None
|
||||
elif k.startswith('static:'):
|
||||
return k[7:]
|
||||
raise ValidationError(_('Invalid setting for column "{header}".').format(header=self.verbose_name))
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
"""
|
||||
Allows you to validate the raw input value for your column. Raise ``ValidationError`` if the value is invalid.
|
||||
You do not need to include the column or row name or value in the error message as it will automatically be
|
||||
included.
|
||||
|
||||
:param value: Contains the raw value of your column as returned by ``resolve``. This can usually be ``None``,
|
||||
e.g. if the column is empty or does not exist in this row.
|
||||
:param previous_values: Dictionary containing the validated values of all columns that have already been validated.
|
||||
"""
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
"""
|
||||
This will be called to perform the actual import. You are supposed to set attributes on the ``order``, ``position``,
|
||||
or ``invoice_address`` objects based on the input ``value``. This is called *before* the actual database
|
||||
transaction, so these three objects do not yet have a primary key. If you want to create related objects, you
|
||||
need to place them into some sort of internal queue and persist them when ``save`` is called.
|
||||
"""
|
||||
pass
|
||||
|
||||
def save(self, order):
|
||||
"""
|
||||
This will be called to perform the actual import. This is called inside the actual database transaction and the
|
||||
input object ``order`` has already been saved to the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class EmailColumn(ImportColumn):
|
||||
identifier = 'email'
|
||||
verbose_name = gettext_lazy('E-mail address')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
EmailValidator()(value)
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
order.email = value
|
||||
|
||||
|
||||
class SubeventColumn(ImportColumn):
|
||||
identifier = 'subevent'
|
||||
verbose_name = pgettext_lazy('subevents', 'Date')
|
||||
default_value = None
|
||||
|
||||
@cached_property
|
||||
def subevents(self):
|
||||
return list(self.event.subevents.filter(active=True).order_by('date_from'))
|
||||
|
||||
def static_choices(self):
|
||||
return [
|
||||
(str(p.pk), str(p)) for p in self.subevents
|
||||
]
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if not value:
|
||||
raise ValidationError(pgettext("subevent", "You need to select a date."))
|
||||
matches = [
|
||||
p for p in self.subevents
|
||||
if str(p.pk) == value or any(
|
||||
(v and v == value) for v in i18n_flat(p.name)) or p.date_from.isoformat() == value
|
||||
]
|
||||
if len(matches) == 0:
|
||||
raise ValidationError(pgettext("subevent", "No matching date was found."))
|
||||
if len(matches) > 1:
|
||||
raise ValidationError(pgettext("subevent", "Multiple matching dates were found."))
|
||||
return matches[0]
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.subevent = value
|
||||
|
||||
|
||||
def i18n_flat(l):
|
||||
if isinstance(l.data, dict):
|
||||
return l.data.values()
|
||||
return [l.data]
|
||||
|
||||
|
||||
class ItemColumn(ImportColumn):
|
||||
identifier = 'item'
|
||||
verbose_name = gettext_lazy('Product')
|
||||
default_value = None
|
||||
|
||||
@cached_property
|
||||
def items(self):
|
||||
return list(self.event.items.filter(active=True))
|
||||
|
||||
def static_choices(self):
|
||||
return [
|
||||
(str(p.pk), str(p)) for p in self.items
|
||||
]
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
matches = [
|
||||
p for p in self.items
|
||||
if str(p.pk) == value or (p.internal_name and p.internal_name == value) or any(
|
||||
(v and v == value) for v in i18n_flat(p.name))
|
||||
]
|
||||
if len(matches) == 0:
|
||||
raise ValidationError(_("No matching product was found."))
|
||||
if len(matches) > 1:
|
||||
raise ValidationError(_("Multiple matching products were found."))
|
||||
return matches[0]
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.item = value
|
||||
|
||||
|
||||
class Variation(ImportColumn):
|
||||
identifier = 'variation'
|
||||
verbose_name = gettext_lazy('Product variation')
|
||||
|
||||
@cached_property
|
||||
def items(self):
|
||||
return list(ItemVariation.objects.filter(
|
||||
active=True, item__active=True, item__event=self.event
|
||||
).select_related('item'))
|
||||
|
||||
def static_choices(self):
|
||||
return [
|
||||
(str(p.pk), '{} – {}'.format(p.item, p.value)) for p in self.items
|
||||
]
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
matches = [
|
||||
p for p in self.items
|
||||
if str(p.pk) == value or any((v and v == value) for v in i18n_flat(p.value)) and p.item_id == previous_values['item'].pk
|
||||
]
|
||||
if len(matches) == 0:
|
||||
raise ValidationError(_("No matching variation was found."))
|
||||
if len(matches) > 1:
|
||||
raise ValidationError(_("Multiple matching variations were found."))
|
||||
return matches[0]
|
||||
elif previous_values['item'].variations.exists():
|
||||
raise ValidationError(_("You need to select a variation for this product."))
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.variation = value
|
||||
|
||||
|
||||
class InvoiceAddressCompany(ImportColumn):
|
||||
identifier = 'invoice_address_company'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Invoice address') + ': ' + _('Company')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
invoice_address.company = value or ''
|
||||
invoice_address.is_business = bool(value)
|
||||
|
||||
|
||||
class InvoiceAddressNamePart(ImportColumn):
|
||||
def __init__(self, event, key, label):
|
||||
self.key = key
|
||||
self.label = label
|
||||
super().__init__(event)
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Invoice address') + ': ' + str(self.label)
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return 'invoice_address_name_{}'.format(self.key)
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
invoice_address.name_parts[self.key] = value or ''
|
||||
|
||||
|
||||
class InvoiceAddressStreet(ImportColumn):
|
||||
identifier = 'invoice_address_street'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Invoice address') + ': ' + _('Address')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
invoice_address.address = value or ''
|
||||
|
||||
|
||||
class InvoiceAddressZip(ImportColumn):
|
||||
identifier = 'invoice_address_zipcode'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Invoice address') + ': ' + _('ZIP code')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
invoice_address.zipcode = value or ''
|
||||
|
||||
|
||||
class InvoiceAddressCity(ImportColumn):
|
||||
identifier = 'invoice_address_city'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Invoice address') + ': ' + _('City')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
invoice_address.city = value or ''
|
||||
|
||||
|
||||
class InvoiceAddressCountry(ImportColumn):
|
||||
identifier = 'invoice_address_country'
|
||||
default_value = None
|
||||
|
||||
@property
|
||||
def initial(self):
|
||||
return 'static:' + str(guess_country(self.event))
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Invoice address') + ': ' + _('Country')
|
||||
|
||||
def static_choices(self):
|
||||
return list(countries)
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value and not Country(value).numeric:
|
||||
raise ValidationError(_("Please enter a valid country code."))
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
invoice_address.country = value or ''
|
||||
|
||||
|
||||
class InvoiceAddressState(ImportColumn):
|
||||
identifier = 'invoice_address_state'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Invoice address') + ': ' + _('State')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
if previous_values.get('invoice_address_country') not in COUNTRIES_WITH_STATE_IN_ADDRESS:
|
||||
raise ValidationError(_("States are not supported for this country."))
|
||||
|
||||
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[previous_values.get('invoice_address_country')]
|
||||
match = [
|
||||
s for s in pycountry.subdivisions.get(country_code=previous_values.get('invoice_address_country'))
|
||||
if s.type in types and (s.code[3:] == value or s.name == value)
|
||||
]
|
||||
if len(match) == 0:
|
||||
raise ValidationError(_("Please enter a valid state."))
|
||||
return match[0].code[3:]
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
invoice_address.state = value or ''
|
||||
|
||||
|
||||
class InvoiceAddressVATID(ImportColumn):
|
||||
identifier = 'invoice_address_vat_id'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Invoice address') + ': ' + _('VAT ID')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
invoice_address.vat_id = value or ''
|
||||
|
||||
|
||||
class InvoiceAddressReference(ImportColumn):
|
||||
identifier = 'invoice_address_internal_reference'
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Invoice address') + ': ' + _('Internal reference')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
invoice_address.internal_reference = value or ''
|
||||
|
||||
|
||||
class AttendeeNamePart(ImportColumn):
|
||||
def __init__(self, event, key, label):
|
||||
self.key = key
|
||||
self.label = label
|
||||
super().__init__(event)
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Attendee name') + ': ' + str(self.label)
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return 'attendee_name_{}'.format(self.key)
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.attendee_name_parts[self.key] = value or ''
|
||||
|
||||
|
||||
class AttendeeEmail(ImportColumn):
|
||||
identifier = 'attendee_email'
|
||||
verbose_name = gettext_lazy('Attendee e-mail address')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
EmailValidator()(value)
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.attendee_email = value
|
||||
|
||||
|
||||
class Price(ImportColumn):
|
||||
identifier = 'price'
|
||||
verbose_name = gettext_lazy('Price')
|
||||
default_label = gettext_lazy('Calculate from product')
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value not in (None, ''):
|
||||
value = formats.sanitize_separators(re.sub(r'[^0-9.,-]', '', value))
|
||||
try:
|
||||
value = Decimal(value)
|
||||
except (DecimalException, TypeError):
|
||||
raise ValidationError(_('You entered an invalid number.'))
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
if value is None:
|
||||
p = get_price(position.item, position.variation, position.voucher, subevent=position.subevent,
|
||||
invoice_address=invoice_address)
|
||||
else:
|
||||
p = get_price(position.item, position.variation, position.voucher, subevent=position.subevent,
|
||||
invoice_address=invoice_address, custom_price=value, force_custom_price=True)
|
||||
position.price = p.gross
|
||||
position.tax_rule = position.item.tax_rule
|
||||
position.tax_rate = p.rate
|
||||
position.tax_value = p.tax
|
||||
|
||||
|
||||
class Secret(ImportColumn):
|
||||
identifier = 'secret'
|
||||
verbose_name = gettext_lazy('Ticket code')
|
||||
default_label = gettext_lazy('Generate automatically')
|
||||
|
||||
def __init__(self, *args):
|
||||
self._cached = set()
|
||||
super().__init__(*args)
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value and (value in self._cached or OrderPosition.all.filter(order__event=self.event, secret=value).exists()):
|
||||
raise ValidationError(
|
||||
_('You cannot assign a position secret that already exists.')
|
||||
)
|
||||
self._cached.add(value)
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
if value:
|
||||
position.secret = value
|
||||
|
||||
|
||||
class Locale(ImportColumn):
|
||||
identifier = 'locale'
|
||||
verbose_name = gettext_lazy('Order locale')
|
||||
default_value = None
|
||||
|
||||
@property
|
||||
def initial(self):
|
||||
return 'static:' + self.event.settings.locale
|
||||
|
||||
def static_choices(self):
|
||||
locale_names = dict(settings.LANGUAGES)
|
||||
return [
|
||||
(a, locale_names[a]) for a in self.event.settings.locales
|
||||
]
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if not value:
|
||||
value = self.event.settings.locale
|
||||
if value not in self.event.settings.locales:
|
||||
raise ValidationError(_("Please enter a valid language code."))
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
order.locale = value
|
||||
|
||||
|
||||
class Saleschannel(ImportColumn):
|
||||
identifier = 'sales_channel'
|
||||
verbose_name = gettext_lazy('Sales channel')
|
||||
|
||||
def static_choices(self):
|
||||
return [
|
||||
(sc.identifier, sc.verbose_name) for sc in get_all_sales_channels().values()
|
||||
]
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if not value:
|
||||
value = 'web'
|
||||
if value not in get_all_sales_channels():
|
||||
raise ValidationError(_("Please enter a valid sales channel."))
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
order.sales_channel = value
|
||||
|
||||
|
||||
class SeatColumn(ImportColumn):
|
||||
identifier = 'seat'
|
||||
verbose_name = gettext_lazy('Seat ID')
|
||||
|
||||
def __init__(self, *args):
|
||||
self._cached = set()
|
||||
super().__init__(*args)
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
try:
|
||||
value = Seat.objects.get(
|
||||
seat_guid=value,
|
||||
subevent=previous_values.get('subevent')
|
||||
)
|
||||
except Seat.DoesNotExist:
|
||||
raise ValidationError(_('No matching seat was found.'))
|
||||
if not value.is_available() or value in self._cached:
|
||||
raise ValidationError(
|
||||
_('The seat you selected has already been taken. Please select a different seat.'))
|
||||
self._cached.add(value)
|
||||
elif previous_values['item'].seat_category_mappings.filter(subevent=previous_values.get('subevent')).exists():
|
||||
raise ValidationError(_('You need to select a specific seat.'))
|
||||
return value
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
position.seat = value
|
||||
|
||||
|
||||
class Comment(ImportColumn):
|
||||
identifier = 'comment'
|
||||
verbose_name = gettext_lazy('Comment')
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
order.comment = value or ''
|
||||
|
||||
|
||||
class QuestionColumn(ImportColumn):
|
||||
def __init__(self, event, q):
|
||||
self.q = q
|
||||
super().__init__(event)
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('Question') + ': ' + str(self.q.question)
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return 'question_{}'.format(self.q.pk)
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
return self.q.clean_answer(value)
|
||||
|
||||
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||
if value:
|
||||
if not hasattr(order, '_answers'):
|
||||
order._answers = []
|
||||
if isinstance(value, QuestionOption):
|
||||
a = QuestionAnswer(orderposition=position, question=self.q, answer=str(value))
|
||||
a._options = [value]
|
||||
order._answers.append(a)
|
||||
elif isinstance(value, list):
|
||||
a = QuestionAnswer(orderposition=position, question=self.q, answer=', '.join(str(v) for v in value))
|
||||
a._options = value
|
||||
order._answers.append(a)
|
||||
else:
|
||||
order._answers.append(QuestionAnswer(question=self.q, answer=str(value), orderposition=position))
|
||||
|
||||
def save(self, order):
|
||||
for a in getattr(order, '_answers', []):
|
||||
a.orderposition = a.orderposition # This is apparently required after save() again
|
||||
a.save()
|
||||
if hasattr(a, '_options'):
|
||||
a.options.add(*a._options)
|
||||
|
||||
|
||||
def get_all_columns(event):
|
||||
default = []
|
||||
if event.has_subevents:
|
||||
default.append(SubeventColumn(event))
|
||||
default += [
|
||||
EmailColumn(event),
|
||||
ItemColumn(event),
|
||||
Variation(event),
|
||||
InvoiceAddressCompany(event),
|
||||
]
|
||||
scheme = PERSON_NAME_SCHEMES.get(event.settings.name_scheme)
|
||||
for n, l, w in scheme['fields']:
|
||||
default.append(InvoiceAddressNamePart(event, n, l))
|
||||
default += [
|
||||
InvoiceAddressStreet(event),
|
||||
InvoiceAddressZip(event),
|
||||
InvoiceAddressCity(event),
|
||||
InvoiceAddressCountry(event),
|
||||
InvoiceAddressState(event),
|
||||
InvoiceAddressVATID(event),
|
||||
InvoiceAddressReference(event),
|
||||
]
|
||||
for n, l, w in scheme['fields']:
|
||||
default.append(AttendeeNamePart(event, n, l))
|
||||
default += [
|
||||
AttendeeEmail(event),
|
||||
Price(event),
|
||||
Secret(event),
|
||||
Locale(event),
|
||||
Saleschannel(event),
|
||||
SeatColumn(event),
|
||||
Comment(event)
|
||||
]
|
||||
for q in event.questions.exclude(type='F'):
|
||||
default.append(QuestionColumn(event, q))
|
||||
|
||||
for recv, resp in order_import_columns.send(sender=event):
|
||||
default += resp
|
||||
|
||||
return default
|
||||
@@ -10,6 +10,8 @@ from functools import partial
|
||||
from io import BytesIO
|
||||
|
||||
import bleach
|
||||
from arabic_reshaper import ArabicReshaper
|
||||
from bidi.algorithm import get_display
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.dispatch import receiver
|
||||
@@ -32,6 +34,7 @@ from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import Paragraph
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.invoice import ThumbnailingImageReader
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
@@ -198,6 +201,11 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"editor_sample": _("Sample company"),
|
||||
"evaluate": lambda op, order, ev: escape(order.invoice_address.company if getattr(order, 'invoice_address', None) else '')
|
||||
}),
|
||||
("invoice_city", {
|
||||
"label": _("Invoice address city"),
|
||||
"editor_sample": _("Sample city"),
|
||||
"evaluate": lambda op, order, ev: escape(order.invoice_address.city if getattr(order, 'invoice_address', None) else '')
|
||||
}),
|
||||
("addons", {
|
||||
"label": _("List of Add-Ons"),
|
||||
"editor_sample": _("Addon 1\nAddon 2"),
|
||||
@@ -405,7 +413,11 @@ class Renderer:
|
||||
def _get_ev(self, op, order):
|
||||
return op.subevent or order.event
|
||||
|
||||
def _get_text_content(self, op: OrderPosition, order: Order, o: dict):
|
||||
def _get_text_content(self, op: OrderPosition, order: Order, o: dict, inner=False):
|
||||
if o.get('locale', None) and not inner:
|
||||
with language(o['locale']):
|
||||
return self._get_text_content(op, order, o, True)
|
||||
|
||||
ev = self._get_ev(op, order)
|
||||
if not o['content']:
|
||||
return '(error)'
|
||||
@@ -449,11 +461,32 @@ class Renderer:
|
||||
tags=["br"], attributes={}, styles=[], strip=True
|
||||
)
|
||||
)
|
||||
|
||||
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
||||
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||
configuration = {
|
||||
'delete_harakat': True,
|
||||
'support_ligatures': False,
|
||||
}
|
||||
reshaper = ArabicReshaper(configuration=configuration)
|
||||
text = "<br/>".join(get_display(reshaper.reshape(l)) for l in text.split("<br/>"))
|
||||
|
||||
p = Paragraph(text, style=style)
|
||||
p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
|
||||
w, h = p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
|
||||
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
|
||||
ad = getAscentDescent(font, float(o['fontsize']))
|
||||
p.drawOn(canvas, float(o['left']) * mm, float(o['bottom']) * mm - ad[1])
|
||||
canvas.saveState()
|
||||
# The ascent/descent offsets here are not really proven to be correct, they're just empirical values to get
|
||||
# reportlab render similarly to browser canvas.
|
||||
if o.get('downward', False):
|
||||
canvas.translate(float(o['left']) * mm, float(o['bottom']) * mm)
|
||||
canvas.rotate(o.get('rotation', 0) * -1)
|
||||
p.drawOn(canvas, 0, -h - ad[1] / 2)
|
||||
else:
|
||||
canvas.translate(float(o['left']) * mm, float(o['bottom']) * mm + h)
|
||||
canvas.rotate(o.get('rotation', 0) * -1)
|
||||
p.drawOn(canvas, 0, -h - ad[1])
|
||||
canvas.restoreState()
|
||||
|
||||
def draw_page(self, canvas: Canvas, order: Order, op: OrderPosition):
|
||||
for o in self.layout:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
@@ -44,13 +45,14 @@ def get_all_plugins(event=None) -> List[type]:
|
||||
|
||||
|
||||
class PluginConfig(AppConfig):
|
||||
IGNORE = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not hasattr(self, 'PretixPluginMeta'):
|
||||
raise ImproperlyConfigured("A pretix plugin config should have a PretixPluginMeta inner class.")
|
||||
|
||||
if hasattr(self.PretixPluginMeta, 'compatibility'):
|
||||
if hasattr(self.PretixPluginMeta, 'compatibility') and not os.environ.get("PRETIX_IGNORE_CONFLICTS") == "True":
|
||||
import pkg_resources
|
||||
try:
|
||||
pkg_resources.require(self.PretixPluginMeta.compatibility)
|
||||
|
||||
@@ -15,7 +15,7 @@ from django_scopes import scopes_disabled
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, InvoiceAddress, Item, ItemBundle, ItemVariation, Seat,
|
||||
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Seat,
|
||||
SeatCategoryMapping, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
@@ -82,6 +82,9 @@ error_messages = {
|
||||
'voucher_expired': _('This voucher is expired.'),
|
||||
'voucher_invalid_item': _('This voucher is not valid for this product.'),
|
||||
'voucher_invalid_seat': _('This voucher is not valid for this seat.'),
|
||||
'voucher_no_match': _('We did not find any position in your cart that we could use this voucher for. If you want '
|
||||
'to add something new to your cart using that voucher, you can do so with the voucher '
|
||||
'redemption option on the bottom of the page.'),
|
||||
'voucher_item_not_available': _(
|
||||
'Your voucher is valid for a product that is currently not for sale.'),
|
||||
'voucher_invalid_subevent': pgettext_lazy('subevent', 'This voucher is not valid for this event date.'),
|
||||
@@ -105,12 +108,15 @@ error_messages = {
|
||||
|
||||
class CartManager:
|
||||
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'price', 'voucher', 'quotas',
|
||||
'addon_to', 'subevent', 'includes_tax', 'bundled', 'seat'))
|
||||
'addon_to', 'subevent', 'includes_tax', 'bundled', 'seat',
|
||||
'price_before_voucher'))
|
||||
RemoveOperation = namedtuple('RemoveOperation', ('position',))
|
||||
VoucherOperation = namedtuple('VoucherOperation', ('position', 'voucher', 'price'))
|
||||
ExtendOperation = namedtuple('ExtendOperation', ('position', 'count', 'item', 'variation', 'price', 'voucher',
|
||||
'quotas', 'subevent', 'seat'))
|
||||
'quotas', 'subevent', 'seat', 'price_before_voucher'))
|
||||
order = {
|
||||
RemoveOperation: 10,
|
||||
VoucherOperation: 15,
|
||||
ExtendOperation: 20,
|
||||
AddOperation: 30
|
||||
}
|
||||
@@ -228,7 +234,7 @@ class CartManager:
|
||||
# TODO: i18n plurals
|
||||
raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,))
|
||||
|
||||
def _check_item_constraints(self, op):
|
||||
def _check_item_constraints(self, op, current_ops=[]):
|
||||
if isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
|
||||
if not (
|
||||
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
|
||||
@@ -271,7 +277,7 @@ class CartManager:
|
||||
raise CartError(error_messages['ended'])
|
||||
|
||||
seated = self._is_seated(op.item, op.subevent)
|
||||
if seated and (not op.seat or op.seat.blocked):
|
||||
if seated and (not op.seat or (op.seat.blocked and self._sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel)):
|
||||
raise CartError(error_messages['seat_invalid'])
|
||||
elif op.seat and not seated:
|
||||
raise CartError(error_messages['seat_forbidden'])
|
||||
@@ -300,10 +306,10 @@ class CartManager:
|
||||
if op.item.max_per_order or op.item.min_per_order:
|
||||
new_total = (
|
||||
len([1 for p in self.positions if p.item_id == op.item.pk]) +
|
||||
sum([_op.count for _op in self._operations
|
||||
sum([_op.count for _op in self._operations + current_ops
|
||||
if isinstance(_op, self.AddOperation) and _op.item == op.item]) +
|
||||
op.count -
|
||||
len([1 for _op in self._operations
|
||||
len([1 for _op in self._operations + current_ops
|
||||
if isinstance(_op, self.RemoveOperation) and _op.position.item_id == op.item.pk])
|
||||
)
|
||||
|
||||
@@ -364,10 +370,10 @@ class CartManager:
|
||||
cp.item.requires_seat = cp.requires_seat
|
||||
|
||||
if cp.is_bundled:
|
||||
try:
|
||||
bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation)
|
||||
bundle = cp.addon_to.item.bundles.filter(bundled_item=cp.item, bundled_variation=cp.variation).first()
|
||||
if bundle:
|
||||
price = bundle.designated_price or 0
|
||||
except ItemBundle.DoesNotExist:
|
||||
else:
|
||||
price = cp.price
|
||||
|
||||
changed_prices[cp.pk] = price
|
||||
@@ -379,6 +385,7 @@ class CartManager:
|
||||
else:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, price, cp.subevent,
|
||||
force_custom_price=True)
|
||||
pbv = TAXED_ZERO
|
||||
else:
|
||||
bundled_sum = Decimal('0.00')
|
||||
if not cp.addon_to_id:
|
||||
@@ -391,9 +398,14 @@ class CartManager:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
|
||||
cp_is_net=True, bundled_sum=bundled_sum)
|
||||
price = TaxedPrice(net=price.net, gross=price.net, rate=0, tax=0, name='')
|
||||
pbv = self._get_price(cp.item, cp.variation, None, cp.price, cp.subevent,
|
||||
cp_is_net=True, bundled_sum=bundled_sum)
|
||||
pbv = TaxedPrice(net=pbv.net, gross=pbv.net, rate=0, tax=0, name='')
|
||||
else:
|
||||
price = self._get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent,
|
||||
bundled_sum=bundled_sum)
|
||||
pbv = self._get_price(cp.item, cp.variation, None, cp.price, cp.subevent,
|
||||
bundled_sum=bundled_sum)
|
||||
|
||||
quotas = list(cp.quotas)
|
||||
if not quotas:
|
||||
@@ -409,7 +421,7 @@ class CartManager:
|
||||
|
||||
op = self.ExtendOperation(
|
||||
position=cp, item=cp.item, variation=cp.variation, voucher=cp.voucher, count=1,
|
||||
price=price, quotas=quotas, subevent=cp.subevent, seat=cp.seat
|
||||
price=price, quotas=quotas, subevent=cp.subevent, seat=cp.seat, price_before_voucher=pbv
|
||||
)
|
||||
self._check_item_constraints(op)
|
||||
|
||||
@@ -419,6 +431,60 @@ class CartManager:
|
||||
self._operations.append(op)
|
||||
return err
|
||||
|
||||
def apply_voucher(self, voucher_code: str):
|
||||
if self._operations:
|
||||
raise CartError('Applying a voucher to the whole cart should not be combined with other operations.')
|
||||
try:
|
||||
voucher = self.event.vouchers.get(code__iexact=voucher_code.strip())
|
||||
except Voucher.DoesNotExist:
|
||||
raise CartError(error_messages['voucher_invalid'])
|
||||
voucher_use_diff = Counter()
|
||||
ops = []
|
||||
|
||||
if not voucher.is_active():
|
||||
raise CartError(error_messages['voucher_expired'])
|
||||
|
||||
for p in self.positions:
|
||||
if p.voucher_id:
|
||||
continue
|
||||
|
||||
if not voucher.applies_to(p.item, p.variation):
|
||||
continue
|
||||
|
||||
if voucher.seat and voucher.seat != p.seat:
|
||||
continue
|
||||
|
||||
if voucher.subevent_id and voucher.subevent_id != p.subevent_id:
|
||||
continue
|
||||
|
||||
if p.is_bundled:
|
||||
continue
|
||||
|
||||
bundled_sum = Decimal('0.00')
|
||||
if not p.addon_to_id:
|
||||
for bundledp in p.addons.all():
|
||||
if bundledp.is_bundled:
|
||||
bundledprice = bundledp.price
|
||||
bundled_sum += bundledprice
|
||||
|
||||
price = self._get_price(p.item, p.variation, voucher, None, p.subevent, bundled_sum=bundled_sum)
|
||||
"""
|
||||
if price.gross > p.price:
|
||||
continue
|
||||
"""
|
||||
|
||||
voucher_use_diff[voucher] += 1
|
||||
ops.append((p.price - price.gross, self.VoucherOperation(p, voucher, price)))
|
||||
|
||||
# If there are not enough voucher usages left for the full cart, let's apply them in the order that benefits
|
||||
# the user the most.
|
||||
ops.sort(key=lambda k: k[0], reverse=True)
|
||||
self._operations += [k[1] for k in ops]\
|
||||
|
||||
if not voucher_use_diff:
|
||||
raise CartError(error_messages['voucher_no_match'])
|
||||
self._voucher_use_diff += voucher_use_diff
|
||||
|
||||
def add_new_items(self, items: List[dict]):
|
||||
# Fetch items from the database
|
||||
self._update_items_cache([i['item'] for i in items], [i['variation'] for i in items])
|
||||
@@ -429,7 +495,7 @@ class CartManager:
|
||||
|
||||
for i in items:
|
||||
if self.event.has_subevents:
|
||||
if not i.get('subevent'):
|
||||
if not i.get('subevent') or int(i.get('subevent')) not in self._subevents_cache:
|
||||
raise CartError(error_messages['subevent_required'])
|
||||
subevent = self._subevents_cache[int(i.get('subevent'))]
|
||||
else:
|
||||
@@ -510,18 +576,20 @@ class CartManager:
|
||||
bop = self.AddOperation(
|
||||
count=bundle.count, item=bitem, variation=bvar, price=bprice,
|
||||
voucher=None, quotas=bundle_quotas, addon_to='FAKE', subevent=subevent,
|
||||
includes_tax=bool(bprice.rate), bundled=[], seat=None
|
||||
includes_tax=bool(bprice.rate), bundled=[], seat=None, price_before_voucher=bprice,
|
||||
)
|
||||
self._check_item_constraints(bop)
|
||||
self._check_item_constraints(bop, operations)
|
||||
bundled.append(bop)
|
||||
|
||||
price = self._get_price(item, variation, voucher, i.get('price'), subevent, bundled_sum=bundled_sum)
|
||||
pbv = self._get_price(item, variation, None, i.get('price'), subevent, bundled_sum=bundled_sum)
|
||||
|
||||
op = self.AddOperation(
|
||||
count=i['count'], item=item, variation=variation, price=price, voucher=voucher, quotas=quotas,
|
||||
addon_to=False, subevent=subevent, includes_tax=bool(price.rate), bundled=bundled, seat=seat
|
||||
addon_to=False, subevent=subevent, includes_tax=bool(price.rate), bundled=bundled, seat=seat,
|
||||
price_before_voucher=pbv
|
||||
)
|
||||
self._check_item_constraints(op)
|
||||
self._check_item_constraints(op, operations)
|
||||
operations.append(op)
|
||||
|
||||
self._quota_diff.update(quota_diff)
|
||||
@@ -625,9 +693,10 @@ class CartManager:
|
||||
|
||||
op = self.AddOperation(
|
||||
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
|
||||
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=None
|
||||
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=None,
|
||||
price_before_voucher=None
|
||||
)
|
||||
self._check_item_constraints(op)
|
||||
self._check_item_constraints(op, operations)
|
||||
operations.append(op)
|
||||
|
||||
# Check constraints on the add-on combinations
|
||||
@@ -762,7 +831,7 @@ class CartManager:
|
||||
self._operations.sort(key=lambda a: self.order[type(a)])
|
||||
seats_seen = set()
|
||||
|
||||
for op in self._operations:
|
||||
for iop, op in enumerate(self._operations):
|
||||
if isinstance(op, self.RemoveOperation):
|
||||
if op.position.expires > self.now_dt:
|
||||
for q in op.position.quotas:
|
||||
@@ -830,7 +899,7 @@ class CartManager:
|
||||
available_count = 0
|
||||
|
||||
if isinstance(op, self.AddOperation):
|
||||
if op.seat and not op.seat.is_available(ignore_voucher_id=op.voucher.id if op.voucher else None):
|
||||
if op.seat and not op.seat.is_available(ignore_voucher_id=op.voucher.id if op.voucher else None, sales_channel=self._sales_channel):
|
||||
available_count = 0
|
||||
err = err or error_messages['seat_unavailable']
|
||||
|
||||
@@ -839,7 +908,8 @@ class CartManager:
|
||||
event=self.event, item=op.item, variation=op.variation,
|
||||
price=op.price.gross, expires=self._expiry, cart_id=self.cart_id,
|
||||
voucher=op.voucher, addon_to=op.addon_to if op.addon_to else None,
|
||||
subevent=op.subevent, includes_tax=op.includes_tax, seat=op.seat
|
||||
subevent=op.subevent, includes_tax=op.includes_tax, seat=op.seat,
|
||||
price_before_voucher=op.price_before_voucher.gross if op.price_before_voucher is not None else None
|
||||
)
|
||||
if self.event.settings.attendee_names_asked:
|
||||
scheme = PERSON_NAME_SCHEMES.get(self.event.settings.name_scheme)
|
||||
@@ -878,7 +948,7 @@ class CartManager:
|
||||
|
||||
new_cart_positions.append(cp)
|
||||
elif isinstance(op, self.ExtendOperation):
|
||||
if op.seat and not op.seat.is_available(ignore_cart=op.position,
|
||||
if op.seat and not op.seat.is_available(ignore_cart=op.position, sales_channel=self._sales_channel,
|
||||
ignore_voucher_id=op.position.voucher_id):
|
||||
err = err or error_messages['seat_unavailable']
|
||||
op.position.addons.all().delete()
|
||||
@@ -886,6 +956,8 @@ class CartManager:
|
||||
elif available_count == 1:
|
||||
op.position.expires = self._expiry
|
||||
op.position.price = op.price.gross
|
||||
if op.price_before_voucher is not None:
|
||||
op.position.price_before_voucher = op.price_before_voucher.gross
|
||||
try:
|
||||
op.position.save(force_update=True)
|
||||
except DatabaseError:
|
||||
@@ -896,6 +968,20 @@ class CartManager:
|
||||
op.position.delete()
|
||||
else:
|
||||
raise AssertionError("ExtendOperation cannot affect more than one item")
|
||||
elif isinstance(op, self.VoucherOperation):
|
||||
if vouchers_ok[op.voucher] < 1:
|
||||
if iop == 0:
|
||||
raise CartError(error_messages['voucher_redeemed'])
|
||||
else:
|
||||
# We fail silently if we could only apply the voucher to part of the cart, since that might
|
||||
# be expected
|
||||
continue
|
||||
|
||||
op.position.price_before_voucher = op.position.price
|
||||
op.position.price = op.price.gross
|
||||
op.position.voucher = op.voucher
|
||||
op.position.save()
|
||||
vouchers_ok[op.voucher] -= 1
|
||||
|
||||
for p in new_cart_positions:
|
||||
if getattr(p, '_answers', None):
|
||||
@@ -1061,7 +1147,27 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en') -> None:
|
||||
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web') -> None:
|
||||
"""
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
:param voucher: A voucher code
|
||||
:param session: Session ID of a guest
|
||||
"""
|
||||
with language(locale):
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||
cm.apply_voucher(voucher)
|
||||
cm.commit()
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
raise CartError(error_messages['busy'])
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web') -> None:
|
||||
"""
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
@@ -1071,7 +1177,7 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
|
||||
with language(locale):
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id)
|
||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||
cm.remove_item(position)
|
||||
cm.commit()
|
||||
except LockTimeoutException:
|
||||
@@ -1081,7 +1187,7 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def clear_cart(self, event: Event, cart_id: str=None, locale='en') -> None:
|
||||
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web') -> None:
|
||||
"""
|
||||
Removes a list of items from a user's cart.
|
||||
:param event: The event ID in question
|
||||
@@ -1090,7 +1196,7 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en') -> None:
|
||||
with language(locale):
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id)
|
||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||
cm.clear()
|
||||
cm.commit()
|
||||
except LockTimeoutException:
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext, ugettext as _
|
||||
from django_countries.fields import Country
|
||||
@@ -52,11 +53,17 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString)
|
||||
if lp and lp.payment_provider:
|
||||
if 'payment' in inspect.signature(lp.payment_provider.render_invoice_text).parameters:
|
||||
payment = lp.payment_provider.render_invoice_text(invoice.order, lp)
|
||||
payment = str(lp.payment_provider.render_invoice_text(invoice.order, lp))
|
||||
else:
|
||||
payment = lp.payment_provider.render_invoice_text(invoice.order)
|
||||
payment = str(lp.payment_provider.render_invoice_text(invoice.order))
|
||||
else:
|
||||
payment = ""
|
||||
if invoice.event.settings.invoice_include_expire_date and invoice.order.status == Order.STATUS_PENDING:
|
||||
if payment:
|
||||
payment += "<br />"
|
||||
payment += pgettext("invoice", "Please complete your payment before {expire_date}.").format(
|
||||
expire_date=date_format(invoice.order.expires, "SHORT_DATE_FORMAT")
|
||||
)
|
||||
|
||||
invoice.introductory_text = str(introductory).replace('\n', '<br />')
|
||||
invoice.additional_text = str(additional).replace('\n', '<br />')
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import smtplib
|
||||
import ssl
|
||||
import warnings
|
||||
from email.mime.image import MIMEImage
|
||||
from email.utils import formataddr
|
||||
@@ -132,7 +133,7 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||
else:
|
||||
sender = formataddr((settings.PRETIX_INSTANCE_NAME, sender))
|
||||
|
||||
subject = str(subject)
|
||||
subject = raw_subject = str(subject)
|
||||
signature = ""
|
||||
|
||||
bcc = []
|
||||
@@ -198,13 +199,13 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
|
||||
|
||||
try:
|
||||
if 'position' in inspect.signature(renderer.render).parameters:
|
||||
body_html = renderer.render(content_plain, signature, str(subject), order, position)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
||||
else:
|
||||
# Backwards compatibility
|
||||
warnings.warn('E-mail renderer called without position argument because position argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, str(subject), order)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
||||
except:
|
||||
logger.exception('Could not render HTML body')
|
||||
body_html = None
|
||||
@@ -365,6 +366,8 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
|
||||
raise SendMailException('Failed to send an email to {}.'.format(to))
|
||||
except Exception as e:
|
||||
if isinstance(e, (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError, ssl.SSLError, OSError)):
|
||||
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 2))
|
||||
if order:
|
||||
order.log_action(
|
||||
'pretix.event.order.email.error',
|
||||
|
||||
173
src/pretix/base/services/orderimport.py
Normal file
173
src/pretix/base/services/orderimport.py
Normal file
@@ -0,0 +1,173 @@
|
||||
import csv
|
||||
import io
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from pretix.base.i18n import LazyLocaleException, language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Event, InvoiceAddress, Order, OrderPayment, OrderPosition,
|
||||
User,
|
||||
)
|
||||
from pretix.base.orderimport import get_all_columns
|
||||
from pretix.base.services.invoices import generate_invoice, invoice_qualified
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.base.signals import order_paid, order_placed
|
||||
from pretix.celery_app import app
|
||||
|
||||
|
||||
class DataImportError(LazyLocaleException):
|
||||
def __init__(self, *args):
|
||||
msg = args[0]
|
||||
msgargs = args[1] if len(args) > 1 else None
|
||||
self.args = args
|
||||
if msgargs:
|
||||
msg = _(msg) % msgargs
|
||||
else:
|
||||
msg = _(msg)
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
def parse_csv(file, length=None):
|
||||
data = file.read(length)
|
||||
try:
|
||||
import chardet
|
||||
charset = chardet.detect(data)['encoding']
|
||||
except ImportError:
|
||||
charset = file.charset
|
||||
data = data.decode(charset or 'utf-8')
|
||||
# If the file was modified on a Mac, it only contains \r as line breaks
|
||||
if '\r' in data and '\n' not in data:
|
||||
data = data.replace('\r', '\n')
|
||||
|
||||
dialect = csv.Sniffer().sniff(data.split("\n")[0], delimiters=";,.#:")
|
||||
if dialect is None:
|
||||
return None
|
||||
|
||||
reader = csv.DictReader(io.StringIO(data), dialect=dialect)
|
||||
return reader
|
||||
|
||||
|
||||
def setif(record, obj, attr, setting):
|
||||
if setting.startswith('csv:'):
|
||||
setattr(obj, attr, record[setting[4:]] or '')
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, throws=(DataImportError,))
|
||||
def import_orders(event: Event, fileid: str, settings: dict, locale: str, user) -> None:
|
||||
# TODO: quotacheck?
|
||||
cf = CachedFile.objects.get(id=fileid)
|
||||
user = User.objects.get(pk=user)
|
||||
with language(locale):
|
||||
cols = get_all_columns(event)
|
||||
parsed = parse_csv(cf.file)
|
||||
orders = []
|
||||
order = None
|
||||
data = []
|
||||
|
||||
# Run validation
|
||||
for i, record in enumerate(parsed):
|
||||
values = {}
|
||||
for c in cols:
|
||||
val = c.resolve(settings, record)
|
||||
try:
|
||||
values[c.identifier] = c.clean(val, values)
|
||||
except ValidationError as e:
|
||||
raise DataImportError(
|
||||
_(
|
||||
'Error while importing value "{value}" for column "{column}" in line "{line}": {message}').format(
|
||||
value=val if val is not None else '', column=c.verbose_name, line=i + 1, message=e.message
|
||||
)
|
||||
)
|
||||
data.append(values)
|
||||
|
||||
# Prepare model objects. Yes, this might consume lots of RAM, but allows us to make the actual SQL transaction
|
||||
# shorter. We'll see what works better in reality…
|
||||
for i, record in enumerate(data):
|
||||
try:
|
||||
if order is None or settings['orders'] == 'many':
|
||||
order = Order(
|
||||
event=event,
|
||||
testmode=settings['testmode'],
|
||||
)
|
||||
order.meta_info = {}
|
||||
order._positions = []
|
||||
order._address = InvoiceAddress()
|
||||
order._address.name_parts = {'_scheme': event.settings.name_scheme}
|
||||
orders.append(order)
|
||||
|
||||
position = OrderPosition()
|
||||
position.attendee_name_parts = {'_scheme': event.settings.name_scheme}
|
||||
position.meta_info = {}
|
||||
order._positions.append(position)
|
||||
position.assign_pseudonymization_id()
|
||||
|
||||
for c in cols:
|
||||
c.assign(record.get(c.identifier), order, position, order._address)
|
||||
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
_('Invalid data in row {row}: {message}').format(row=i, message=str(e))
|
||||
)
|
||||
|
||||
# quota check?
|
||||
with event.lock():
|
||||
with transaction.atomic():
|
||||
for o in orders:
|
||||
o.total = sum([c.price for c in o._positions]) # currently no support for fees
|
||||
if o.total == Decimal('0.00'):
|
||||
o.status = Order.STATUS_PAID
|
||||
o.save()
|
||||
OrderPayment.objects.create(
|
||||
local_id=1,
|
||||
order=o,
|
||||
amount=Decimal('0.00'),
|
||||
provider='free',
|
||||
info='{}',
|
||||
payment_date=now(),
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
)
|
||||
elif settings['status'] == 'paid':
|
||||
o.status = Order.STATUS_PAID
|
||||
o.save()
|
||||
OrderPayment.objects.create(
|
||||
local_id=1,
|
||||
order=o,
|
||||
amount=o.total,
|
||||
provider='manual',
|
||||
info='{}',
|
||||
payment_date=now(),
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
)
|
||||
else:
|
||||
o.status = Order.STATUS_PENDING
|
||||
o.save()
|
||||
for p in o._positions:
|
||||
p.order = o
|
||||
p.save()
|
||||
o._address.order = o
|
||||
o._address.save()
|
||||
for c in cols:
|
||||
c.save(o)
|
||||
o.log_action(
|
||||
'pretix.event.order.placed',
|
||||
user=user,
|
||||
data={'source': 'import'}
|
||||
)
|
||||
|
||||
for o in orders:
|
||||
with language(o.locale):
|
||||
order_placed.send(event, order=o)
|
||||
if o.status == Order.STATUS_PAID:
|
||||
order_paid.send(event, order=o)
|
||||
|
||||
gen_invoice = invoice_qualified(o) and (
|
||||
(event.settings.get('invoice_generate') == 'True') or
|
||||
(event.settings.get('invoice_generate') == 'paid' and o.status == Order.STATUS_PAID)
|
||||
) and not o.invoices.last()
|
||||
if gen_invoice:
|
||||
generate_invoice(o, trigger_pdf=True)
|
||||
cf.delete()
|
||||
@@ -70,6 +70,8 @@ error_messages = {
|
||||
'voucher_invalid': _('The voucher code used for one of the items in your cart is not known in our database.'),
|
||||
'voucher_redeemed': _('The voucher code used for one of the items in your cart has already been used the maximum '
|
||||
'number of times allowed. We removed this item from your cart.'),
|
||||
'voucher_budget_used': _('The voucher code used for one of the items in your cart has already been too often. We '
|
||||
'adjusted the price of the item in your cart.'),
|
||||
'voucher_expired': _('The voucher code used for one of the items in your cart is expired. We removed this item '
|
||||
'from your cart.'),
|
||||
'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We '
|
||||
@@ -418,13 +420,15 @@ def _check_date(event: Event, now_dt: datetime):
|
||||
raise OrderError(error_messages['ended'])
|
||||
|
||||
|
||||
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None):
|
||||
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None,
|
||||
sales_channel='web'):
|
||||
err = None
|
||||
errargs = None
|
||||
_check_date(event, now_dt)
|
||||
|
||||
products_seen = Counter()
|
||||
changed_prices = {}
|
||||
v_budget = {}
|
||||
deleted_positions = set()
|
||||
seats_seen = set()
|
||||
|
||||
@@ -467,6 +471,20 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.voucher.budget is not None:
|
||||
if cp.voucher not in v_budget:
|
||||
v_budget[cp.voucher] = cp.voucher.budget - cp.voucher.budget_used()
|
||||
disc = cp.price_before_voucher - cp.price
|
||||
if disc > v_budget[cp.voucher]:
|
||||
new_disc = max(0, v_budget[cp.voucher])
|
||||
cp.price = cp.price + (disc - new_disc)
|
||||
cp.save()
|
||||
err = err or error_messages['voucher_budget_used']
|
||||
v_budget[cp.voucher] -= new_disc
|
||||
continue
|
||||
else:
|
||||
v_budget[cp.voucher] -= disc
|
||||
|
||||
if cp.subevent and cp.subevent.presale_start and now_dt < cp.subevent.presale_start:
|
||||
err = err or error_messages['some_subevent_not_started']
|
||||
delete(cp)
|
||||
@@ -512,7 +530,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
if cp.seat:
|
||||
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every
|
||||
# time, since we absolutely can not overbook a seat.
|
||||
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id) or cp.seat.blocked:
|
||||
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel):
|
||||
err = err or error_messages['seat_unavailable']
|
||||
cp.delete()
|
||||
continue
|
||||
@@ -521,6 +539,11 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
# Other checks are not necessary
|
||||
continue
|
||||
|
||||
max_discount = None
|
||||
if cp.price_before_voucher is not None and cp.voucher in v_budget:
|
||||
current_discount = cp.price_before_voucher - cp.price
|
||||
max_discount = max(v_budget[cp.voucher] + current_discount, 0)
|
||||
|
||||
if cp.is_bundled:
|
||||
try:
|
||||
bundle = cp.addon_to.item.bundles.get(bundled_item=cp.item, bundled_variation=cp.variation)
|
||||
@@ -528,7 +551,9 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
except ItemBundle.DoesNotExist:
|
||||
bprice = cp.price
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
|
||||
invoice_address=address, force_custom_price=True)
|
||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||
pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
|
||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||
changed_prices[cp.pk] = bprice
|
||||
else:
|
||||
bundled_sum = 0
|
||||
@@ -538,7 +563,14 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
bundled_sum += changed_prices.get(bundledp.pk, bundledp.price)
|
||||
|
||||
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
|
||||
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum)
|
||||
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
|
||||
max_discount=max_discount)
|
||||
pbv = get_price(cp.item, cp.variation, None, cp.price, cp.subevent, custom_price_is_net=False,
|
||||
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
|
||||
max_discount=max_discount)
|
||||
|
||||
if max_discount is not None:
|
||||
v_budget[cp.voucher] = v_budget[cp.voucher] + current_discount - (pbv.gross - price.gross)
|
||||
|
||||
if price is False or len(quotas) == 0:
|
||||
err = err or error_messages['unavailable']
|
||||
@@ -551,6 +583,11 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if pbv is not None and pbv.gross != price.gross:
|
||||
cp.price_before_voucher = pbv.gross
|
||||
else:
|
||||
cp.price_before_voucher = None
|
||||
|
||||
if price.gross != cp.price and not (cp.item.free_price and cp.price > price.gross):
|
||||
cp.price = price.gross
|
||||
cp.includes_tax = bool(price.rate)
|
||||
@@ -801,12 +838,15 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
|
||||
lockfn = event.lock
|
||||
|
||||
with lockfn() as now_dt:
|
||||
positions = list(positions.select_related('item', 'variation', 'subevent', 'seat', 'addon_to').prefetch_related('addons'))
|
||||
positions = list(
|
||||
positions.select_related('item', 'variation', 'subevent', 'seat', 'addon_to').prefetch_related('addons')
|
||||
)
|
||||
positions.sort(key=lambda k: position_ids.index(k.pk))
|
||||
if len(positions) == 0:
|
||||
raise OrderError(error_messages['empty'])
|
||||
if len(position_ids) != len(positions):
|
||||
raise OrderError(error_messages['internal'])
|
||||
_check_positions(event, now_dt, positions, address=addr)
|
||||
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel)
|
||||
order, payment = _create_order(event, email, positions, now_dt, pprov,
|
||||
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
|
||||
gift_cards=gift_cards, shown_total=shown_total)
|
||||
@@ -944,7 +984,7 @@ def send_download_reminders(sender, **kwargs):
|
||||
continue
|
||||
|
||||
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if now() < reminder_date:
|
||||
if now() < reminder_date or o.datetime > reminder_date:
|
||||
continue
|
||||
|
||||
with transaction.atomic():
|
||||
@@ -1064,6 +1104,21 @@ class OrderChangeManager:
|
||||
self._operations.append(self.ItemOperation(position, item, variation))
|
||||
|
||||
def change_seat(self, position: OrderPosition, seat: Seat):
|
||||
if isinstance(seat, str):
|
||||
subev = None
|
||||
if self.event.has_subevents:
|
||||
subev = position.subevent
|
||||
for p in self._operations:
|
||||
if isinstance(p, self.SubeventOperation) and p.position == position:
|
||||
subev = p.subevent
|
||||
try:
|
||||
seat = Seat.objects.get(
|
||||
event=self.event,
|
||||
subevent=subev,
|
||||
seat_guid=seat
|
||||
)
|
||||
except Seat.DoesNotExist:
|
||||
raise OrderError(error_messages['seat_invalid'])
|
||||
if position.seat:
|
||||
self._seatdiff.subtract([position.seat])
|
||||
if seat:
|
||||
@@ -1147,6 +1202,19 @@ class OrderChangeManager:
|
||||
|
||||
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: Order = None,
|
||||
subevent: SubEvent = None, seat: Seat = None):
|
||||
if isinstance(seat, str):
|
||||
if not seat:
|
||||
seat = None
|
||||
else:
|
||||
try:
|
||||
seat = Seat.objects.get(
|
||||
event=self.event,
|
||||
subevent=subevent,
|
||||
seat_guid=seat
|
||||
)
|
||||
except Seat.DoesNotExist:
|
||||
raise OrderError(error_messages['seat_invalid'])
|
||||
|
||||
if price is None:
|
||||
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
|
||||
else:
|
||||
@@ -1170,7 +1238,7 @@ class OrderChangeManager:
|
||||
raise OrderError(self.error_messages['seat_required'])
|
||||
elif not seated and seat:
|
||||
raise OrderError(self.error_messages['seat_forbidden'])
|
||||
if seat and subevent and seat.subevent_id != subevent:
|
||||
if seat and subevent and seat.subevent_id != subevent.pk:
|
||||
raise OrderError(self.error_messages['seat_subevent_mismatch'].format(seat=seat.name))
|
||||
|
||||
new_quotas = (variation.quotas.filter(subevent=subevent)
|
||||
@@ -1197,7 +1265,7 @@ class OrderChangeManager:
|
||||
for seat, diff in self._seatdiff.items():
|
||||
if diff <= 0:
|
||||
continue
|
||||
if not seat.is_available() or diff > 1:
|
||||
if not seat.is_available(sales_channel=self.order.sales_channel) or diff > 1:
|
||||
raise OrderError(self.error_messages['seat_unavailable'].format(seat=seat.name))
|
||||
|
||||
if self.event.has_subevents:
|
||||
@@ -1333,6 +1401,16 @@ class OrderChangeManager:
|
||||
op.position.item = op.item
|
||||
op.position.variation = op.variation
|
||||
op.position._calculate_tax()
|
||||
if op.position.price_before_voucher is not None and op.position.voucher and not op.position.addon_to_id:
|
||||
op.position.price_before_voucher = max(
|
||||
op.position.price,
|
||||
get_price(
|
||||
op.position.item, op.position.variation,
|
||||
subevent=op.position.subevent,
|
||||
custom_price=op.position.price,
|
||||
invoice_address=self._invoice_address
|
||||
).gross
|
||||
)
|
||||
op.position.save()
|
||||
elif isinstance(op, self.SeatOperation):
|
||||
self.order.log_action('pretix.event.order.changed.seat', user=self.user, auth=self.auth, data={
|
||||
@@ -1356,6 +1434,16 @@ class OrderChangeManager:
|
||||
})
|
||||
op.position.subevent = op.subevent
|
||||
op.position.save()
|
||||
if op.position.price_before_voucher is not None and op.position.voucher and not op.position.addon_to_id:
|
||||
op.position.price_before_voucher = max(
|
||||
op.position.price,
|
||||
get_price(
|
||||
op.position.item, op.position.variation,
|
||||
subevent=op.position.subevent,
|
||||
custom_price=op.position.price,
|
||||
invoice_address=self._invoice_address
|
||||
).gross
|
||||
)
|
||||
elif isinstance(op, self.FeeValueOperation):
|
||||
self.order.log_action('pretix.event.order.changed.feevalue', user=self.user, auth=self.auth, data={
|
||||
'fee': op.fee.pk,
|
||||
|
||||
@@ -12,7 +12,8 @@ def get_price(item: Item, variation: ItemVariation = None,
|
||||
voucher: Voucher = None, custom_price: Decimal = None,
|
||||
subevent: SubEvent = None, custom_price_is_net: bool = False,
|
||||
addon_to: AbstractPosition = None, invoice_address: InvoiceAddress = None,
|
||||
force_custom_price: bool = False, bundled_sum: Decimal = Decimal('0.00')) -> TaxedPrice:
|
||||
force_custom_price: bool = False, bundled_sum: Decimal = Decimal('0.00'),
|
||||
max_discount: Decimal = None) -> TaxedPrice:
|
||||
if addon_to:
|
||||
try:
|
||||
iao = addon_to.item.addons.get(addon_category_id=item.category_id)
|
||||
@@ -32,7 +33,7 @@ def get_price(item: Item, variation: ItemVariation = None,
|
||||
price = subevent.var_price_overrides[variation.pk]
|
||||
|
||||
if voucher:
|
||||
price = voucher.calculate_price(price)
|
||||
price = voucher.calculate_price(price, max_discount=max_discount)
|
||||
|
||||
if item.tax_rule:
|
||||
tax_rule = item.tax_rule
|
||||
|
||||
@@ -85,6 +85,10 @@ DEFAULTS = {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
},
|
||||
'invoice_include_expire_date': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
},
|
||||
'invoice_numbers_consecutive': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
@@ -679,6 +683,10 @@ Your {event} team"""))
|
||||
)),
|
||||
'type': LazyI18nString
|
||||
},
|
||||
'order_import_settings': {
|
||||
'default': '{}',
|
||||
'type': dict
|
||||
},
|
||||
'organizer_info_text': {
|
||||
'default': '',
|
||||
'type': LazyI18nString
|
||||
@@ -742,7 +750,15 @@ Your {event} team"""))
|
||||
'name_scheme': {
|
||||
'default': 'full',
|
||||
'type': str
|
||||
}
|
||||
},
|
||||
'giftcard_length': {
|
||||
'default': settings.ENTROPY['giftcard_secret'],
|
||||
'type': int
|
||||
},
|
||||
'seating_allow_blocked_seats_for_channel': {
|
||||
'default': [],
|
||||
'type': list
|
||||
},
|
||||
}
|
||||
PERSON_NAME_TITLE_GROUPS = OrderedDict([
|
||||
('english_common', (_('Most common English titles'), (
|
||||
|
||||
@@ -630,3 +630,14 @@ invoice_line_text = EventPluginSignal(
|
||||
This signal is sent out when an invoice is built for an order. You can return additional text that
|
||||
should be shown on the invoice for the given ``position``.
|
||||
"""
|
||||
|
||||
order_import_columns = EventPluginSignal(
|
||||
providing_args=[]
|
||||
)
|
||||
"""
|
||||
This signal is sent out if the user performs an import of orders from an external source. You can use this
|
||||
to define additional columns that can be read during import. You are expected to return a list of instances of
|
||||
``ImportColumn`` subclasses.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
@@ -139,6 +139,14 @@
|
||||
text-decoration: none;
|
||||
color: {{ color }};
|
||||
}
|
||||
{% if rtl %}
|
||||
body {
|
||||
direction: rtl;
|
||||
}
|
||||
.content table td {
|
||||
text-align: right;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% block addcss %}{% endblock %}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator, validate_email
|
||||
from django.core.validators import (
|
||||
MaxValueValidator, MinValueValidator, RegexValidator, validate_email,
|
||||
)
|
||||
from django.db.models import Q
|
||||
from django.forms import formset_factory
|
||||
from django.urls import reverse
|
||||
@@ -146,7 +149,8 @@ class EventWizardBasicsForm(I18nModelForm):
|
||||
self.user = kwargs.pop('user')
|
||||
kwargs.pop('session')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.initial['timezone'] = get_current_timezone_name()
|
||||
if 'timezone' not in self.initial:
|
||||
self.initial['timezone'] = get_current_timezone_name()
|
||||
self.fields['locale'].choices = [(a, b) for a, b in settings.LANGUAGES if a in self.locales]
|
||||
self.fields['location'].widget.attrs['rows'] = '3'
|
||||
self.fields['location'].widget.attrs['placeholder'] = _(
|
||||
@@ -270,12 +274,18 @@ class EventMetaValueForm(forms.ModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['value'].required = False
|
||||
self.fields['value'].widget.attrs['placeholder'] = self.property.default
|
||||
self.fields['value'].widget.attrs['data-typeahead-url'] = (
|
||||
reverse('control:events.meta.typeahead') + '?' + urlencode({
|
||||
'property': self.property.name,
|
||||
'organizer': self.property.organizer.slug,
|
||||
})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = EventMetaValue
|
||||
fields = ['value']
|
||||
widgets = {
|
||||
'value': forms.TextInput
|
||||
'value': forms.TextInput()
|
||||
}
|
||||
|
||||
|
||||
@@ -673,6 +683,9 @@ class PaymentSettingsForm(SettingsForm):
|
||||
"you use slow payment methods like bank transfer, we recommend 14 days. If you only use real-time "
|
||||
"payment methods, we recommend still setting two or three days to allow people to retry failed "
|
||||
"payments."),
|
||||
validators=[MinValueValidator(0),
|
||||
MaxValueValidator(1000000)]
|
||||
|
||||
)
|
||||
payment_term_last = RelativeDateField(
|
||||
label=_('Last date of payments'),
|
||||
@@ -837,7 +850,9 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
help_text=_("This will be prepended to invoice numbers. If you leave this field empty, your event slug will "
|
||||
"be used followed by a dash. Attention: If multiple events within the same organization use the "
|
||||
"same value in this field, they will share their number range, i.e. every full number will be "
|
||||
"used at most once over all of your events. This setting only affects future invoices."),
|
||||
"used at most once over all of your events. This setting only affects future invoices. You can "
|
||||
"use %Y (with century) %y (without century) to insert the year of the invoice, or %m and %d for "
|
||||
"the day of month."),
|
||||
required=False,
|
||||
)
|
||||
invoice_numbers_prefix_cancellations = forms.CharField(
|
||||
@@ -870,6 +885,11 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
label=_("Show attendee names on invoices"),
|
||||
required=False
|
||||
)
|
||||
invoice_include_expire_date = forms.BooleanField(
|
||||
label=_("Show expiration date of order"),
|
||||
help_text=_("The expiration date will not be shown if the invoice is generated after the order is paid."),
|
||||
required=False
|
||||
)
|
||||
invoice_email_attachment = forms.BooleanField(
|
||||
label=_("Attach invoices to emails"),
|
||||
help_text=_("If invoices are automatically generated for all orders, they will be attached to the order "
|
||||
@@ -1433,14 +1453,8 @@ class WidgetCodeForm(forms.Form):
|
||||
|
||||
class EventDeleteForm(forms.Form):
|
||||
error_messages = {
|
||||
'pw_current_wrong': _("The password you entered was not correct."),
|
||||
'slug_wrong': _("The slug you entered was not correct."),
|
||||
}
|
||||
user_pw = forms.CharField(
|
||||
max_length=255,
|
||||
label=_("Your password"),
|
||||
widget=forms.PasswordInput()
|
||||
)
|
||||
slug = forms.CharField(
|
||||
max_length=255,
|
||||
label=_("Event slug"),
|
||||
@@ -1448,19 +1462,8 @@ class EventDeleteForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
self.user = kwargs.pop('user')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_user_pw(self):
|
||||
user_pw = self.cleaned_data.get('user_pw')
|
||||
if not check_password(user_pw, self.user.password):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['pw_current_wrong'],
|
||||
code='pw_current_wrong',
|
||||
)
|
||||
|
||||
return user_pw
|
||||
|
||||
def clean_slug(self):
|
||||
slug = self.cleaned_data.get('slug')
|
||||
if slug != self.event.slug:
|
||||
|
||||
53
src/pretix/control/forms/orderimport.py
Normal file
53
src/pretix/control/forms/orderimport.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.services.orderimport import get_all_columns
|
||||
|
||||
|
||||
class ProcessForm(forms.Form):
|
||||
orders = forms.ChoiceField(
|
||||
label=_('Import mode'),
|
||||
choices=(
|
||||
('many', _('Create a separate order for each line')),
|
||||
('one', _('Create one order with one position per line')),
|
||||
)
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
label=_('Order status'),
|
||||
choices=(
|
||||
('paid', _('Create orders as fully paid')),
|
||||
('pending', _('Create orders as pending and still require payment')),
|
||||
)
|
||||
)
|
||||
testmode = forms.BooleanField(
|
||||
label=_('Create orders as test mode orders'),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
headers = kwargs.pop('headers')
|
||||
initital = kwargs.pop('initial', {})
|
||||
self.event = kwargs.pop('event')
|
||||
initital['testmode'] = self.event.testmode
|
||||
kwargs['initial'] = initital
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
header_choices = [
|
||||
('csv:{}'.format(h), _('CSV column: "{name}"').format(name=h)) for h in headers
|
||||
]
|
||||
|
||||
for c in get_all_columns(self.event):
|
||||
choices = []
|
||||
if c.default_value:
|
||||
choices.append((c.default_value, c.default_label))
|
||||
choices += header_choices
|
||||
for k, v in c.static_choices():
|
||||
choices.append(('static:{}'.format(k), v))
|
||||
|
||||
self.fields[c.identifier] = forms.ChoiceField(
|
||||
label=str(c.verbose_name),
|
||||
choices=choices,
|
||||
widget=forms.Select(
|
||||
attrs={'data-static': 'true'}
|
||||
)
|
||||
)
|
||||
@@ -11,9 +11,7 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from pretix.base.email import get_available_placeholders
|
||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
||||
from pretix.base.forms.widgets import DatePickerWidget
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, ItemAddOn, Order, OrderPosition, Seat,
|
||||
)
|
||||
from pretix.base.models import InvoiceAddress, ItemAddOn, Order, OrderPosition
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.control.forms.widgets import Select2
|
||||
@@ -204,11 +202,10 @@ class OrderPositionAddForm(forms.Form):
|
||||
required=False,
|
||||
label=_('Add-on to'),
|
||||
)
|
||||
seat = forms.ModelChoiceField(
|
||||
Seat.objects.none(),
|
||||
seat = forms.CharField(
|
||||
required=False,
|
||||
label=_('Seat'),
|
||||
empty_label=_('General admission')
|
||||
widget=forms.TextInput(attrs={'placeholder': _('General admission'), 'data-seat-guid-field': 'true'}),
|
||||
label=_('Seat')
|
||||
)
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
@@ -255,19 +252,6 @@ class OrderPositionAddForm(forms.Form):
|
||||
else:
|
||||
del self.fields['addon_to']
|
||||
|
||||
self.fields['seat'].queryset = order.event.seats.all()
|
||||
self.fields['seat'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'seat',
|
||||
'data-select2-url': reverse('control:event.seats.select2', kwargs={
|
||||
'event': order.event.slug,
|
||||
'organizer': order.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('General admission')
|
||||
}
|
||||
)
|
||||
self.fields['seat'].widget.choices = self.fields['seat'].choices
|
||||
|
||||
if order.event.has_subevents:
|
||||
self.fields['subevent'].queryset = order.event.subevents.all()
|
||||
self.fields['subevent'].widget = Select2(
|
||||
@@ -318,10 +302,9 @@ class OrderPositionChangeForm(forms.Form):
|
||||
required=False,
|
||||
empty_label=_('(Unchanged)')
|
||||
)
|
||||
seat = forms.ModelChoiceField(
|
||||
Seat.objects.none(),
|
||||
seat = forms.CharField(
|
||||
required=False,
|
||||
empty_label=_('(Unchanged)')
|
||||
widget=forms.TextInput(attrs={'placeholder': _('(Unchanged)'), 'data-seat-guid-field': 'true'})
|
||||
)
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
@@ -366,20 +349,7 @@ class OrderPositionChangeForm(forms.Form):
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
if instance.seat:
|
||||
self.fields['seat'].queryset = instance.order.event.seats.all()
|
||||
self.fields['seat'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'seat',
|
||||
'data-select2-url': reverse('control:event.seats.select2', kwargs={
|
||||
'event': instance.order.event.slug,
|
||||
'organizer': instance.order.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('(Unchanged)')
|
||||
}
|
||||
)
|
||||
self.fields['seat'].widget.choices = self.fields['seat'].choices
|
||||
else:
|
||||
if not instance.seat:
|
||||
del self.fields['seat']
|
||||
|
||||
choices = [
|
||||
|
||||
@@ -302,6 +302,12 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
|
||||
'We recommend a size of at least 200x200px to accomodate most devices.')
|
||||
)
|
||||
giftcard_length = forms.IntegerField(
|
||||
label=_('Length of gift card codes'),
|
||||
help_text=_('The system generates by default {}-character long gift card codes. However, if a different length '
|
||||
'is required, it can be set here.'.format(settings.ENTROPY['giftcard_secret'])),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django import forms
|
||||
from django.forms import formset_factory
|
||||
from django.urls import reverse
|
||||
from django.utils.dates import MONTHS, WEEKDAYS
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
@@ -171,6 +173,12 @@ class SubEventMetaValueForm(forms.ModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['value'].required = False
|
||||
self.fields['value'].widget.attrs['placeholder'] = self.default or self.property.default
|
||||
self.fields['value'].widget.attrs['data-typeahead-url'] = (
|
||||
reverse('control:events.meta.typeahead') + '?' + urlencode({
|
||||
'property': self.property.name,
|
||||
'organizer': self.property.organizer.slug,
|
||||
})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SubEventMetaValue
|
||||
|
||||
@@ -38,7 +38,7 @@ class VoucherForm(I18nModelForm):
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag',
|
||||
'comment', 'max_usages', 'price_mode', 'subevent', 'show_hidden_items',
|
||||
'comment', 'max_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
|
||||
]
|
||||
field_classes = {
|
||||
'valid_until': SplitDateTimeField,
|
||||
@@ -268,7 +268,7 @@ class VoucherBulkForm(VoucherForm):
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment',
|
||||
'max_usages', 'price_mode', 'subevent', 'show_hidden_items'
|
||||
'max_usages', 'price_mode', 'subevent', 'show_hidden_items', 'budget'
|
||||
]
|
||||
field_classes = {
|
||||
'valid_until': SplitDateTimeField,
|
||||
|
||||
@@ -178,6 +178,7 @@ def _display_checkin(event, logentry):
|
||||
@receiver(signal=logentry_display, dispatch_uid="pretixcontrol_logentry_display")
|
||||
def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
plains = {
|
||||
'pretix.object.cloned': _('This object has been created by cloning.'),
|
||||
'pretix.event.comment': _('The event\'s internal comment has been updated.'),
|
||||
'pretix.event.order.modified': _('The order details have been changed.'),
|
||||
'pretix.event.order.unpaid': _('The order has been marked as unpaid.'),
|
||||
@@ -324,6 +325,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.device.initialized': _('The device has been initialized.'),
|
||||
'pretix.device.keyroll': _('The access token of the device has been regenerated.'),
|
||||
'pretix.device.updated': _('The device has notified the server of an hardware or software update.'),
|
||||
'pretix.giftcards.created': _('The gift card has been created.'),
|
||||
'pretix.giftcards.transaction.manual': _('A manual transaction has been performed.'),
|
||||
}
|
||||
|
||||
data = json.loads(logentry.data)
|
||||
|
||||
@@ -169,6 +169,57 @@ def get_event_navigation(request: HttpRequest):
|
||||
})
|
||||
|
||||
if 'can_view_orders' in request.eventpermset:
|
||||
children = [
|
||||
{
|
||||
'label': _('All orders'),
|
||||
'url': reverse('control:event.orders', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name in ('event.orders', 'event.order') or "event.order." in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Overview'),
|
||||
'url': reverse('control:event.orders.overview', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.orders.overview' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Refunds'),
|
||||
'url': reverse('control:event.orders.refunds', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.orders.refunds' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Export'),
|
||||
'url': reverse('control:event.orders.export', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.orders.export' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Waiting list'),
|
||||
'url': reverse('control:event.orders.waitinglist', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.orders.waitinglist' in url.url_name,
|
||||
},
|
||||
]
|
||||
if 'can_change_orders' in request.eventpermset:
|
||||
children.append({
|
||||
'label': _('Import'),
|
||||
'url': reverse('control:event.orders.import', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.orders.import' in url.url_name,
|
||||
})
|
||||
nav.append({
|
||||
'label': _('Orders'),
|
||||
'url': reverse('control:event.orders', kwargs={
|
||||
@@ -177,48 +228,7 @@ def get_event_navigation(request: HttpRequest):
|
||||
}),
|
||||
'active': False,
|
||||
'icon': 'shopping-cart',
|
||||
'children': [
|
||||
{
|
||||
'label': _('All orders'),
|
||||
'url': reverse('control:event.orders', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name in ('event.orders', 'event.order') or "event.order." in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Overview'),
|
||||
'url': reverse('control:event.orders.overview', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.orders.overview' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Refunds'),
|
||||
'url': reverse('control:event.orders.refunds', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.orders.refunds' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Export'),
|
||||
'url': reverse('control:event.orders.export', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.orders.export' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Waiting list'),
|
||||
'url': reverse('control:event.orders.waitinglist', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.orders.waitinglist' in url.url_name,
|
||||
},
|
||||
]
|
||||
'children': children
|
||||
})
|
||||
|
||||
if 'can_view_vouchers' in request.eventpermset:
|
||||
|
||||
@@ -22,9 +22,16 @@
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-group buttons">
|
||||
{% if backend.login_form_fields %}
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
{% trans "Log in" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if backend.url %}
|
||||
<a href="{{ backend.url }}" class="btn btn-primary btn-block">
|
||||
{{ backend.verbose_name }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if backend.identifier == "native" %}
|
||||
{% if can_reset %}
|
||||
<a href="{% url "control:auth.forgot" %}" class="btn btn-link btn-block">
|
||||
|
||||
@@ -24,12 +24,6 @@
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% bootstrap_field form.slug layout="inline" %}
|
||||
<p>
|
||||
{% blocktrans trimmed with slug=request.event.slug %}
|
||||
Also, to make sure it's really you, please enter your user password here:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% bootstrap_field form.user_pw layout="inline" %}
|
||||
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
<legend>{% trans "Invoice customization" %}</legend>
|
||||
{% bootstrap_field form.invoice_renderer layout="control" %}
|
||||
{% bootstrap_field form.invoice_attendee_name layout="control" %}
|
||||
{% bootstrap_field form.invoice_include_expire_date layout="control" %}
|
||||
{% bootstrap_field form.invoice_introductory_text layout="control" %}
|
||||
{% bootstrap_field form.invoice_additional_text layout="control" %}
|
||||
{% bootstrap_field form.invoice_footer_text layout="control" %}
|
||||
|
||||
@@ -73,13 +73,22 @@
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-order-change" data-pricecalc-endpoint="{% url "api-v1:orderposition-price_calc" organizer=order.event.organizer.slug event=order.event.slug pk=position.pk %}" {% if position.subevent %}data-subevent="{{ position.subevent.id }}{% endif %}">
|
||||
<div class="form-order-change" data-pricecalc-endpoint="{% url "api-v1:orderposition-price_calc" organizer=order.event.organizer.slug event=order.event.slug pk=position.pk %}" {% if position.subevent %}data-subevent="{{ position.subevent.id }}"{% endif %} data-position="{{ position.pk }}">
|
||||
{% bootstrap_form_errors position.form %}
|
||||
{% if position.custom_error %}
|
||||
<div class="alert alert-danger">
|
||||
{{ position.custom_error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if position.voucher and position.voucher.budget %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
This position has been created with a voucher with a limited budget. If you
|
||||
change the price or item, the discount will still be calculated from the original
|
||||
price at the time of purchase.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-sm-offset-3">
|
||||
<strong>{% trans "Current value" %}</strong>
|
||||
@@ -181,7 +190,7 @@
|
||||
{% bootstrap_formset_errors add_formset %}
|
||||
<div data-formset-body>
|
||||
{% for add_form in add_formset %}
|
||||
<div class="panel panel-default items" data-formset-form>
|
||||
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<button type="button" class="btn btn-danger btn-xs pull-right flip"
|
||||
@@ -219,7 +228,7 @@
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default items" data-formset-form>
|
||||
<div class="panel panel-default items" data-formset-form data-subevent="0">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<button type="button" class="btn btn-danger btn-xs pull-right flip"
|
||||
|
||||
@@ -272,7 +272,9 @@
|
||||
{% endif %}
|
||||
{% if line.voucher %}
|
||||
<br/><span class="fa fa-tags"></span> {% trans "Voucher code used:" %}
|
||||
<a href="{% url "control:event.voucher" event=request.event.slug organizer=request.event.organizer.slug voucher=line.voucher.pk %}">
|
||||
<a
|
||||
{% if line.price_before_voucher|default_if_none:"NONE" != "NONE" %}data-toggle="tooltip" title="{% blocktrans trimmed with price=line.price_before_voucher|money:request.event.currency %}Original price: {{ price }}{% endblocktrans %}"{% endif %}
|
||||
href="{% url "control:event.voucher" event=request.event.slug organizer=request.event.organizer.slug voucher=line.voucher.pk %}">
|
||||
{{ line.voucher.code }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load getitem %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Import attendees" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Import attendees" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal" data-asynctask data-asynctask-long>
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Data preview" %}</h3>
|
||||
</div>
|
||||
<div class="table-responsive panel-body">
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for fn in parsed.fieldnames %}
|
||||
<th>{{ fn }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in sample_rows %}
|
||||
<tr>
|
||||
{% for fn in parsed.fieldnames %}
|
||||
<td>{{ r|getitem:fn }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td class="text-center" colspan="{{ parsed.fieldnames|length }}">
|
||||
…
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Import settings" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_form form layout="horizontal" %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
The import will be performed regardless of your quotas, so it will be possible to overbook your event using this option.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Perform import" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Import attendees" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Import attendees" %}</h1>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Upload a new file" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="" method="post" enctype="multipart/form-data" class="form-inline">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
The uploaded file should be a CSV file with a header row. You will be able to assign the
|
||||
meanings of the different columns in the next step.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="file">{% trans "Import file" %}: </label> <input id="file" type="file" name="file"/>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<button class="btn btn-primary pull-right flip" type="submit">
|
||||
<span class="icon icon-upload"></span> {% trans "Start import" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -32,6 +32,7 @@
|
||||
{% endif %}
|
||||
{% bootstrap_field sform.organizer_info_text layout="control" %}
|
||||
{% bootstrap_field sform.event_team_provisioning layout="control" %}
|
||||
{% bootstrap_field sform.giftcard_length layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Organizer page" %}</legend>
|
||||
|
||||
@@ -11,81 +11,95 @@
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<div class="panel panel-primary items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Details" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Gift card code" %}</dt>
|
||||
<dd>{{ card.secret }}</dd>
|
||||
<dt>{% trans "Creation date" %}</dt>
|
||||
<dd>{{ card.issuance|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
<dt>{% trans "Current value" %}</dt>
|
||||
<dd>{{ card.value|money:card.currency }}</dd>
|
||||
<dt>{% trans "Currency" %}</dt>
|
||||
<dd>{{ card.currency }}</dd>
|
||||
{% if card.issued_in %}
|
||||
<dt>{% trans "Issued through sale" %}</dt>
|
||||
<dd>
|
||||
<a href="{% url "control:event.order" event=card.issued_in.order.event.slug organizer=card.issued_in.order.event.organizer.slug code=card.issued_in.order.code %}">
|
||||
{{ card.issued_in.order.full_code }}</a>-{{ card.issued_in.positionid }}
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Transactions" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "Order" %}</th>
|
||||
<th class="text-right">{% trans "Value" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in card.transactions.all %}
|
||||
<tr>
|
||||
<td>{{ t.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>
|
||||
{% if t.order %}
|
||||
<a href="{% url "control:event.order" event=t.order.event.slug organizer=t.order.event.organizer.slug code=t.order.code %}">
|
||||
{{ t.order.full_code }}
|
||||
</a>
|
||||
{% else %}
|
||||
<em>{% trans "Manual transaction" %}</em>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-xs-12">
|
||||
<div class="panel panel-primary items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Details" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Gift card code" %}</dt>
|
||||
<dd>{{ card.secret }}</dd>
|
||||
<dt>{% trans "Creation date" %}</dt>
|
||||
<dd>{{ card.issuance|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
<dt>{% trans "Current value" %}</dt>
|
||||
<dd>{{ card.value|money:card.currency }}</dd>
|
||||
<dt>{% trans "Currency" %}</dt>
|
||||
<dd>{{ card.currency }}</dd>
|
||||
{% if card.issued_in %}
|
||||
<dt>{% trans "Issued through sale" %}</dt>
|
||||
<dd>
|
||||
<a href="{% url "control:event.order" event=card.issued_in.order.event.slug organizer=card.issued_in.order.event.organizer.slug code=card.issued_in.order.code %}">
|
||||
{{ card.issued_in.order.full_code }}</a>-{{ card.issued_in.positionid }}
|
||||
</dd>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ t.value|money:card.currency }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right">
|
||||
<form class="helper-display-inline form-inline" method="post" action="">
|
||||
{% csrf_token %}
|
||||
<input type="text" class="form-control input-sm" placeholder="{% trans "Value" %}" name="value">
|
||||
<button class="btn btn-primary">
|
||||
<span class="fa fa-plus"></span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Transactions" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "Order" %}</th>
|
||||
<th class="text-right">{% trans "Value" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in card.transactions.all %}
|
||||
<tr>
|
||||
<td>{{ t.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>
|
||||
{% if t.order %}
|
||||
<a href="{% url "control:event.order" event=t.order.event.slug organizer=t.order.event.organizer.slug code=t.order.code %}">
|
||||
{{ t.order.full_code }}
|
||||
</a>
|
||||
{% else %}
|
||||
<em>{% trans "Manual transaction" %}</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ t.value|money:card.currency }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right">
|
||||
<form class="helper-display-inline form-inline" method="post" action="">
|
||||
{% csrf_token %}
|
||||
<input type="text" class="form-control input-sm" placeholder="{% trans "Value" %}" name="value">
|
||||
<button class="btn btn-primary">
|
||||
<span class="fa fa-plus"></span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Gift card history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=card %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -198,11 +198,24 @@
|
||||
</div>
|
||||
<div class="col-sm-12 help-inline">
|
||||
<p>
|
||||
After you changed the page size, you need to create a new empty background. If you
|
||||
want to use a custom background, it already needs to have the correct size.
|
||||
{% blocktrans trimmed %}
|
||||
After you changed the page size, you need to create a new empty background. If you
|
||||
want to use a custom background, it already needs to have the correct size.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group pdf-info">
|
||||
<div class="col-sm-12">
|
||||
<label>{% trans "Prefered language" %}</label><br>
|
||||
<select class="form-control" id="pdf-info-locale">
|
||||
<option value="">{% trans "Order locale" %}</option>
|
||||
{% for l in locales %}
|
||||
<option value="{{ l.0 }}">{{ l.1 }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group position">
|
||||
<div class="col-sm-6">
|
||||
<label>{% trans "x (mm)" %}</label><br>
|
||||
@@ -251,6 +264,12 @@
|
||||
<span class="fa fa-italic"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default toggling" data-action="downward"
|
||||
data-toggle="tooltip" title="{% trans "Flow multiple lines downward from specified position" %}">
|
||||
<span class="fa fa-caret-square-o-down"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -293,11 +312,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group text">
|
||||
<div class="col-sm-12">
|
||||
<div class="col-sm-6">
|
||||
<label>{% trans "Width (mm)" %}</label><br>
|
||||
<input type="number" value="13" class="input-block-level form-control" step="0.01"
|
||||
id="toolbox-textwidth">
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label>{% trans "Rotation (°)" %}</label><br>
|
||||
<input type="number" value="0" class="input-block-level form-control" step="0.1"
|
||||
id="toolbox-textrotation">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row control-group poweredby">
|
||||
<div class="col-sm-12">
|
||||
|
||||
@@ -40,12 +40,12 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Step 3: Confirm deletion" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed with event=request.event.name %}
|
||||
{% blocktrans trimmed with event=request.event.name slug=request.event.slug %}
|
||||
Please re-check that you are fully certain that you want to delete the selected categories of data from the event <strong>{{ event }}</strong>.
|
||||
In this case, please enter your user password here:
|
||||
To confirm you really want this, please type out the event's short name ("{{ slug }}") here:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<input type="password" class="form-control" name="password" required placeholder="{% trans "Your password" %}">
|
||||
<input type="text" class="form-control" name="slug" required placeholder="{% trans "Event short name" %}">
|
||||
</fieldset>
|
||||
<input type="hidden" name="file" value="{{ file.pk }}">
|
||||
<div class="form-group submit-group">
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
{% csrf_token %}
|
||||
<h3>{% trans "Welcome back!" %}</h3>
|
||||
<p>
|
||||
{% trans "We just want to make sure it's really you. Please re-enter your password to continue." %}
|
||||
{% if form.backend.url %}
|
||||
{% blocktrans trimmed with login_provider=form.backend.verbose_name %}We just want to make sure it's really you. Please re-authenticate with '{{ login_provider }}'.{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "We just want to make sure it's really you. Please re-enter your password to continue." %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% bootstrap_form form %}
|
||||
<input class="form-control" id="webauthn-response" name="webauthn"
|
||||
@@ -23,9 +27,15 @@
|
||||
</small></p>
|
||||
{% endif %}
|
||||
<div class="form-group text-right flip">
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
{% if form.backend.url %}
|
||||
<a href="{{ form.backend.url }}" class="btn btn-primary btn-block">
|
||||
{% trans "Continue" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="{% url "control:auth.logout" %}" class="btn btn-link btn-block">
|
||||
{% trans "Log in as someone else" %}
|
||||
</a>
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
{% bootstrap_field form.block_quota layout="control" %}
|
||||
{% bootstrap_field form.allow_ignore_quota layout="control" %}
|
||||
{% bootstrap_field form.budget addon_after=request.event.currency layout="control" %}
|
||||
{% bootstrap_field form.tag layout="control" %}
|
||||
{% bootstrap_field form.comment layout="control" %}
|
||||
{% bootstrap_field form.show_hidden_items layout="control" %}
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
{% bootstrap_field form.block_quota layout="control" %}
|
||||
{% bootstrap_field form.allow_ignore_quota layout="control" %}
|
||||
{% bootstrap_field form.budget addon_after=request.event.currency layout="control" %}
|
||||
{% bootstrap_field form.tag layout="control" %}
|
||||
{% bootstrap_field form.comment layout="control" %}
|
||||
{% bootstrap_field form.show_hidden_items layout="control" %}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load urlreplace %}
|
||||
{% load money %}
|
||||
{% block title %}{% trans "Vouchers" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Vouchers" %}</h1>
|
||||
@@ -143,7 +144,15 @@
|
||||
<strong><a href="{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=v.id %}">{{ v.code }}</a></strong>
|
||||
{% if not v.is_active %}</del>{% endif %}
|
||||
</td>
|
||||
<td>{{ v.redeemed }} / {{ v.max_usages }}</td>
|
||||
<td>
|
||||
{{ v.redeemed }} / {{ v.max_usages }}
|
||||
{% if v.budget|default_if_none:"NONE" != "NONE" %}
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
{{ v.budget_used_orders|money:request.event.currency }} / {{ v.budget|money:request.event.currency }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ v.valid_until|date }}</td>
|
||||
<td>
|
||||
{{ v.tag }}
|
||||
|
||||
11
src/pretix/control/templatetags/getitem.py
Normal file
11
src/pretix/control/templatetags/getitem.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name='getitem')
|
||||
def getitem_filter(value, itemname):
|
||||
if not value:
|
||||
return ''
|
||||
|
||||
return value[itemname]
|
||||
@@ -2,8 +2,8 @@ from django.conf.urls import include, url
|
||||
|
||||
from pretix.control.views import (
|
||||
auth, checkin, dashboards, event, geo, global_settings, item, main, oauth,
|
||||
orders, organizer, pdf, search, shredder, subevents, typeahead, user,
|
||||
users, vouchers, waitinglist,
|
||||
orderimport, orders, organizer, pdf, search, shredder, subevents,
|
||||
typeahead, user, users, vouchers, waitinglist,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -147,7 +147,6 @@ urlpatterns = [
|
||||
url(r'^pdf/editor/(?P<filename>[^/]+).pdf$', pdf.PdfView.as_view(), name='pdf.background'),
|
||||
url(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'),
|
||||
url(r'^subevents/select2$', typeahead.subevent_select2, name='event.subevents.select2'),
|
||||
url(r'^seats/select2$', typeahead.seat_select2, name='event.seats.select2'),
|
||||
url(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventUpdate.as_view(), name='event.subevent'),
|
||||
url(r'^subevents/(?P<subevent>\d+)/delete$', subevents.SubEventDelete.as_view(),
|
||||
name='event.subevent.delete'),
|
||||
@@ -257,6 +256,8 @@ urlpatterns = [
|
||||
url(r'^invoice/(?P<invoice>[^/]+)$', orders.InvoiceDownload.as_view(),
|
||||
name='event.invoice.download'),
|
||||
url(r'^orders/overview/$', orders.OverView.as_view(), name='event.orders.overview'),
|
||||
url(r'^orders/import/$', orderimport.ImportView.as_view(), name='event.orders.import'),
|
||||
url(r'^orders/import/(?P<file>[^/]+)/$', orderimport.ProcessView.as_view(), name='event.orders.import.process'),
|
||||
url(r'^orders/export/$', orders.ExportView.as_view(), name='event.orders.export'),
|
||||
url(r'^orders/export/do$', orders.ExportDoView.as_view(), name='event.orders.export.do'),
|
||||
url(r'^orders/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'),
|
||||
|
||||
@@ -44,6 +44,7 @@ from pretix.control.forms.event import (
|
||||
TicketSettingsForm, WidgetCodeForm,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views.user import RecentAuthenticationRequiredMixin
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
from pretix.multidomain.urlreverse import get_domain
|
||||
from pretix.plugins.stripe.payment import StripeSettingsHolder
|
||||
@@ -824,7 +825,7 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
|
||||
})
|
||||
|
||||
|
||||
class EventDelete(EventPermissionRequiredMixin, FormView):
|
||||
class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, FormView):
|
||||
permission = 'can_change_event_settings'
|
||||
template_name = 'pretixcontrol/event/delete.html'
|
||||
form_class = EventDeleteForm
|
||||
@@ -837,7 +838,6 @@ class EventDelete(EventPermissionRequiredMixin, FormView):
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['user'] = self.request.user
|
||||
kwargs['event'] = self.request.event
|
||||
return kwargs
|
||||
|
||||
|
||||
@@ -267,17 +267,19 @@ class EventWizard(SafeSessionWizardView):
|
||||
event.copy_data_from(from_event)
|
||||
elif self.clone_from:
|
||||
event.copy_data_from(self.clone_from)
|
||||
elif event.has_subevents:
|
||||
event.checkin_lists.create(
|
||||
name=str(se),
|
||||
all_products=True,
|
||||
subevent=se
|
||||
)
|
||||
else:
|
||||
event.checkin_lists.create(
|
||||
name=_('Default'),
|
||||
all_products=True
|
||||
)
|
||||
if event.has_subevents:
|
||||
event.checkin_lists.create(
|
||||
name=str(se),
|
||||
all_products=True,
|
||||
subevent=se
|
||||
)
|
||||
else:
|
||||
event.checkin_lists.create(
|
||||
name=_('Default'),
|
||||
all_products=True
|
||||
)
|
||||
event.set_defaults()
|
||||
|
||||
if basics_data['tax_rate']:
|
||||
if not event.settings.tax_rate_default or event.settings.tax_rate_default.rate != basics_data['tax_rate']:
|
||||
|
||||
125
src/pretix/control/views/orderimport.py
Normal file
125
src/pretix/control/views/orderimport.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import FormView, TemplateView
|
||||
|
||||
from pretix.base.models import CachedFile
|
||||
from pretix.base.services.orderimport import import_orders, parse_csv
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.forms.orderimport import ProcessForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImportView(EventPermissionRequiredMixin, TemplateView):
|
||||
template_name = 'pretixcontrol/orders/import_start.html'
|
||||
permission = 'can_change_orders'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'file' not in request.FILES:
|
||||
return redirect(reverse('control:event.orders.import', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
}))
|
||||
if not request.FILES['file'].name.endswith('.csv'):
|
||||
messages.error(request, _('Please only upload CSV files.'))
|
||||
return redirect(reverse('control:event.orders.import', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
}))
|
||||
if request.FILES['file'].size > 1024 * 1024 * 10:
|
||||
messages.error(request, _('Please do not upload files larger than 10 MB.'))
|
||||
return redirect(reverse('control:event.orders.import', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
}))
|
||||
|
||||
cf = CachedFile.objects.create(
|
||||
expires=now() + timedelta(days=1),
|
||||
date=now(),
|
||||
filename='import.csv',
|
||||
type='text/csv',
|
||||
)
|
||||
cf.file.save('import.csv', request.FILES['file'])
|
||||
return redirect(reverse('control:event.orders.import.process', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
'file': cf.id
|
||||
}))
|
||||
|
||||
|
||||
class ProcessView(EventPermissionRequiredMixin, AsyncAction, FormView):
|
||||
permission = 'can_change_orders'
|
||||
template_name = 'pretixcontrol/orders/import_process.html'
|
||||
form_class = ProcessForm
|
||||
task = import_orders
|
||||
known_errortypes = ['DataImportError']
|
||||
|
||||
def get_form_kwargs(self):
|
||||
k = super().get_form_kwargs()
|
||||
k.update({
|
||||
'event': self.request.event,
|
||||
'initial': self.request.event.settings.order_import_settings,
|
||||
'headers': self.parsed.fieldnames
|
||||
})
|
||||
return k
|
||||
|
||||
def form_valid(self, form):
|
||||
self.request.event.settings.order_import_settings = form.cleaned_data
|
||||
return self.do(
|
||||
self.request.event.pk, self.file.id, form.cleaned_data, self.request.LANGUAGE_CODE,
|
||||
self.request.user.pk
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def file(self):
|
||||
return get_object_or_404(CachedFile, pk=self.kwargs.get("file"), filename="import.csv")
|
||||
|
||||
@cached_property
|
||||
def parsed(self):
|
||||
return parse_csv(self.file.file, 1024 * 1024)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
return self.get_result(request)
|
||||
return FormView.get(self, request, *args, **kwargs)
|
||||
|
||||
def get_success_message(self, value):
|
||||
return _('The import was successful.')
|
||||
|
||||
def get_success_url(self, value):
|
||||
return reverse('control:event.orders', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not self.parsed:
|
||||
messages.error(request, _('We\'ve been unable to parse the uploaded file as a CSV file.'))
|
||||
return redirect(reverse('control:event.orders.import', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
}))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_error_url(self):
|
||||
return reverse('control:event.orders.import.process', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.organizer.slug,
|
||||
'file': self.file.id
|
||||
})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['file'] = self.file
|
||||
ctx['parsed'] = self.parsed
|
||||
ctx['sample_rows'] = list(self.parsed)[:3]
|
||||
return ctx
|
||||
@@ -724,6 +724,7 @@ class OrderRefundView(OrderView):
|
||||
currency=self.request.event.currency,
|
||||
testmode=self.order.testmode
|
||||
)
|
||||
giftcard.log_action('pretix.giftcards.created', user=self.request.user, data={})
|
||||
refunds.append(OrderRefund(
|
||||
order=self.order,
|
||||
payment=None,
|
||||
@@ -1303,6 +1304,7 @@ class OrderChange(OrderView):
|
||||
positions = list(self.order.positions.all())
|
||||
for p in positions:
|
||||
p.form = OrderPositionChangeForm(prefix='op-{}'.format(p.pk), instance=p,
|
||||
initial={'seat': p.seat.seat_guid if p.seat else None},
|
||||
data=self.request.POST if self.request.method == "POST" else None)
|
||||
try:
|
||||
ia = self.order.invoice_address
|
||||
@@ -1398,12 +1400,12 @@ class OrderChange(OrderView):
|
||||
if item != p.item or variation != p.variation:
|
||||
ocm.change_item(p, item, variation)
|
||||
|
||||
if p.seat and p.form.cleaned_data['seat'] and p.form.cleaned_data['seat'] != p.seat:
|
||||
ocm.change_seat(p, p.form.cleaned_data['seat'])
|
||||
|
||||
if self.request.event.has_subevents and p.form.cleaned_data['subevent'] and p.form.cleaned_data['subevent'] != p.subevent:
|
||||
ocm.change_subevent(p, p.form.cleaned_data['subevent'])
|
||||
|
||||
if p.seat and p.form.cleaned_data['seat'] and p.form.cleaned_data['seat'] != p.seat.seat_guid:
|
||||
ocm.change_seat(p, p.form.cleaned_data['seat'])
|
||||
|
||||
if p.form.cleaned_data['price'] != p.price:
|
||||
ocm.change_price(p, p.form.cleaned_data['price'])
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ from pretix.base.models import (
|
||||
Device, GiftCard, Organizer, Team, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
|
||||
from pretix.base.models.giftcards import gen_giftcard_secret
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.control.forms.filter import (
|
||||
@@ -688,7 +689,7 @@ class DeviceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.devices.prefetch_related(
|
||||
'limit_events'
|
||||
).order_by('-device_id')
|
||||
).order_by('revoked', '-device_id')
|
||||
|
||||
|
||||
class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||
@@ -1038,7 +1039,8 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
kwargs = super().get_form_kwargs()
|
||||
any_event = self.request.organizer.events.first()
|
||||
kwargs['initial'] = {
|
||||
'currency': any_event.currency if any_event else settings.DEFAULT_CURRENCY
|
||||
'currency': any_event.currency if any_event else settings.DEFAULT_CURRENCY,
|
||||
'secret': gen_giftcard_secret(self.request.organizer.settings.giftcard_length)
|
||||
}
|
||||
kwargs['organizer'] = self.request.organizer
|
||||
return kwargs
|
||||
@@ -1054,9 +1056,11 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
form.instance.transactions.create(
|
||||
value=form.cleaned_data['value']
|
||||
)
|
||||
form.instance.log_action('pretix.giftcards.transaction.manual', user=self.request.user, data={
|
||||
'value': form.cleaned_data['value']
|
||||
})
|
||||
form.instance.log_action('pretix.giftcards.created', user=self.request.user, data={})
|
||||
if form.cleaned_data['value']:
|
||||
form.instance.log_action('pretix.giftcards.transaction.manual', user=self.request.user, data={
|
||||
'value': form.cleaned_data['value']
|
||||
})
|
||||
return redirect(reverse(
|
||||
'control:organizer.giftcard',
|
||||
kwargs={
|
||||
|
||||
@@ -4,6 +4,7 @@ import mimetypes
|
||||
from datetime import timedelta
|
||||
from io import BytesIO
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
@@ -217,6 +218,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
ctx['variables'] = self.get_variables()
|
||||
ctx['layout'] = json.dumps(self.get_current_layout())
|
||||
ctx['title'] = self.title
|
||||
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
|
||||
return ctx
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from pretix.base.services.shredder import export, shred
|
||||
from pretix.base.shredder import ShredError, shred_constraints
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views.user import RecentAuthenticationRequiredMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,7 +27,7 @@ class ShredderMixin:
|
||||
)
|
||||
|
||||
|
||||
class StartShredView(EventPermissionRequiredMixin, ShredderMixin, TemplateView):
|
||||
class StartShredView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, TemplateView):
|
||||
permission = 'can_change_orders'
|
||||
template_name = 'pretixcontrol/shredder/index.html'
|
||||
|
||||
@@ -37,7 +38,7 @@ class StartShredView(EventPermissionRequiredMixin, ShredderMixin, TemplateView):
|
||||
return ctx
|
||||
|
||||
|
||||
class ShredDownloadView(EventPermissionRequiredMixin, ShredderMixin, TemplateView):
|
||||
class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, TemplateView):
|
||||
permission = 'can_change_orders'
|
||||
template_name = 'pretixcontrol/shredder/download.html'
|
||||
|
||||
@@ -48,7 +49,7 @@ class ShredDownloadView(EventPermissionRequiredMixin, ShredderMixin, TemplateVie
|
||||
return ctx
|
||||
|
||||
|
||||
class ShredExportView(EventPermissionRequiredMixin, ShredderMixin, AsyncAction, View):
|
||||
class ShredExportView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, AsyncAction, View):
|
||||
permission = 'can_change_orders'
|
||||
task = export
|
||||
known_errortypes = ['ShredError']
|
||||
@@ -77,7 +78,7 @@ class ShredExportView(EventPermissionRequiredMixin, ShredderMixin, AsyncAction,
|
||||
return self.do(self.request.event.id, request.POST.getlist("shredder"))
|
||||
|
||||
|
||||
class ShredDoView(EventPermissionRequiredMixin, ShredderMixin, AsyncAction, View):
|
||||
class ShredDoView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, AsyncAction, View):
|
||||
permission = 'can_change_orders'
|
||||
task = shred
|
||||
known_errortypes = ['ShredError']
|
||||
@@ -103,7 +104,7 @@ class ShredDoView(EventPermissionRequiredMixin, ShredderMixin, AsyncAction, View
|
||||
if constr:
|
||||
return self.error(ShredError(self.get_error_url()))
|
||||
|
||||
if not self.request.user.check_password(request.POST.get("password")):
|
||||
return self.error(ShredError(_("The current password you entered was not correct.")))
|
||||
if request.event.slug != request.POST.get("slug"):
|
||||
return self.error(ShredError(_("The slug you entered was not correct.")))
|
||||
|
||||
return self.do(self.request.event.id, request.POST.get("file"), request.POST.get("confirm_code"))
|
||||
|
||||
@@ -13,8 +13,7 @@ from django.utils.timezone import make_aware
|
||||
from django.utils.translation import pgettext, ugettext as _
|
||||
|
||||
from pretix.base.models import (
|
||||
EventMetaProperty, EventMetaValue, Order, Organizer, SubEvent, User,
|
||||
Voucher,
|
||||
EventMetaProperty, EventMetaValue, Order, Organizer, User, Voucher,
|
||||
)
|
||||
from pretix.control.forms.event import EventWizardCopyForm
|
||||
from pretix.control.permissions import event_permission_required
|
||||
@@ -225,46 +224,6 @@ def nav_context_list(request):
|
||||
return JsonResponse(doc)
|
||||
|
||||
|
||||
@event_permission_required("can_view_orders")
|
||||
def seat_select2(request, **kwargs):
|
||||
query = request.GET.get('query', '')
|
||||
try:
|
||||
page = int(request.GET.get('page', '1'))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
if request.event.has_subevents:
|
||||
try:
|
||||
qs = request.event.subevents.get(active=True, pk=request.GET.get('subevent', 0)).free_seats()
|
||||
except SubEvent.DoesNotExist:
|
||||
qs = request.event.seats.none()
|
||||
else:
|
||||
qs = request.event.free_seats()
|
||||
qs = qs.filter(
|
||||
Q(name__icontains=query) | Q(seat_guid__icontains=query)
|
||||
).order_by('name').select_related('product', 'subevent')
|
||||
|
||||
total = qs.count()
|
||||
pagesize = 20
|
||||
offset = (page - 1) * pagesize
|
||||
doc = {
|
||||
'results': [
|
||||
{
|
||||
'id': e.pk,
|
||||
'text': '{} ({})'.format(e.name, str(e.product)),
|
||||
'product': e.product_id,
|
||||
'event': str(e.subevent) if e.subevent else ''
|
||||
|
||||
}
|
||||
for e in qs[offset:offset + pagesize]
|
||||
],
|
||||
'pagination': {
|
||||
"more": total >= (offset + pagesize)
|
||||
}
|
||||
}
|
||||
return JsonResponse(doc)
|
||||
|
||||
|
||||
@event_permission_required(None)
|
||||
def subevent_select2(request, **kwargs):
|
||||
query = request.GET.get('query', '')
|
||||
|
||||
@@ -37,9 +37,11 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
||||
permission = 'can_view_vouchers'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.vouchers.filter(waitinglistentries__isnull=True).select_related(
|
||||
qs = Voucher.annotate_budget_used_orders(self.request.event.vouchers.filter(
|
||||
waitinglistentries__isnull=True
|
||||
).select_related(
|
||||
'item', 'variation', 'seat'
|
||||
)
|
||||
))
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
@@ -65,7 +67,7 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
||||
|
||||
headers = [
|
||||
_('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'),
|
||||
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages')
|
||||
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat')
|
||||
]
|
||||
writer.writerow(headers)
|
||||
|
||||
@@ -77,6 +79,8 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
||||
prod = '%s' % str(v.item)
|
||||
elif v.quota:
|
||||
prod = _('Any product in quota "{quota}"').format(quota=str(v.quota.name))
|
||||
else:
|
||||
prod = _('Any product')
|
||||
row = [
|
||||
v.code,
|
||||
v.valid_until.isoformat() if v.valid_until else "",
|
||||
@@ -87,7 +91,8 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
||||
str(v.value) if v.value is not None else "",
|
||||
v.tag,
|
||||
str(v.redeemed),
|
||||
str(v.max_usages)
|
||||
str(v.max_usages),
|
||||
str(v.seat) if v.seat else ""
|
||||
]
|
||||
writer.writerow(row)
|
||||
|
||||
@@ -306,6 +311,7 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
||||
if self.copy_from:
|
||||
i = modelcopy(self.copy_from)
|
||||
i.pk = None
|
||||
i.redeemed = 0
|
||||
kwargs['instance'] = i
|
||||
else:
|
||||
kwargs['instance'] = Voucher(event=self.request.event)
|
||||
|
||||
@@ -6,6 +6,13 @@ from django.conf import settings
|
||||
def set_cookie_without_samesite(request, response, key, *args, **kwargs):
|
||||
assert 'samesite' not in kwargs
|
||||
response.set_cookie(key, *args, **kwargs)
|
||||
is_secure = (
|
||||
kwargs.get('secure', False) or request.scheme == 'https' or
|
||||
settings.SITE_URL.startswith('https://')
|
||||
)
|
||||
if not is_secure:
|
||||
# https://www.chromestatus.com/feature/5633521622188032
|
||||
return
|
||||
if should_send_same_site_none(request.headers.get('User-Agent', '')):
|
||||
# Chromium is rolling out SameSite=Lax as a default
|
||||
# https://www.chromestatus.com/feature/5088147346030592
|
||||
@@ -15,10 +22,7 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
|
||||
response.cookies[key]['samesite'] = 'None'
|
||||
# This will only work on secure cookies as well
|
||||
# https://www.chromestatus.com/feature/5633521622188032
|
||||
response.cookies[key]['secure'] = (
|
||||
kwargs.get('secure', False) or request.scheme == 'https' or
|
||||
settings.SITE_URL.startswith('https://')
|
||||
)
|
||||
response.cookies[key]['secure'] = is_secure
|
||||
|
||||
|
||||
# Based on https://www.chromium.org/updates/same-site/incompatible-clients
|
||||
@@ -48,7 +52,7 @@ def drops_unrecognized_same_site_cookies(useragent):
|
||||
|
||||
# Regex parsing of User-Agent string. (See note above!)
|
||||
RE_CHROMIUM = re.compile(r"Chrom(e|ium)")
|
||||
RE_CHROMIUM_VERSION = re.compile(r"Chrom[^ /]+/([0-9]+)[.0-9]*")
|
||||
RE_CHROMIUM_VERSION = re.compile(r"Chrom[^ /]+[ /]([0-9]+)[.0-9]*")
|
||||
RE_UC_VERSION = re.compile(r"UCBrowser/([0-9]+)\.([0-9]+)\.([0-9]+)[.0-9]* ")
|
||||
RE_IOS_VERSION = re.compile(r"\(iP.+; CPU .*OS ([0-9]+)[_0-9]*.*\) AppleWebKit/")
|
||||
RE_MAC_VERSION = re.compile(r"\(Macintosh;.*Mac OS X ([0-9]+)_([0-9]+)[_0-9]*.*\) AppleWebKit/")
|
||||
@@ -86,7 +90,10 @@ def is_chromium_based(useragent):
|
||||
|
||||
def is_chromium_version_at_least(major, useragent):
|
||||
# Extract digits from first capturing group.
|
||||
version = int(RE_CHROMIUM_VERSION.search(useragent).group(1))
|
||||
match = RE_CHROMIUM_VERSION.search(useragent)
|
||||
if not match:
|
||||
return False
|
||||
version = int(match.group(1))
|
||||
return version >= major
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-12-05 13:34+0000\n"
|
||||
"PO-Revision-Date: 2019-11-18 09:44+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2019-12-25 01:00+0000\n"
|
||||
"Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"ar/>\n"
|
||||
"Language: ar\n"
|
||||
@@ -92,28 +92,22 @@ msgstr "حدث خطأ من نوع {كود}."
|
||||
msgid ""
|
||||
"We currently cannot reach the server, but we keep trying. Last error code: "
|
||||
"{code}"
|
||||
msgstr ""
|
||||
"نحن في الوقت الراهن لا يمكن أن تصل إلى الخادم، ولكننا نواصل المحاولة. رمز "
|
||||
"الخطأ نشاط: {كود}"
|
||||
msgstr "لم نستطع معالجة طلبك، ولكننا نواصل المحاولة. رمز الخطأ : {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
#, fuzzy
|
||||
#| msgid "The request took to long. Please try again."
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "استغرق بناء على طلب لفترة طويلة. حاول مرة اخرى."
|
||||
msgstr "استغرقت العملية فترة طويلة، الرجاء المحاولة مرة أخرى."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:150
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:26
|
||||
msgid ""
|
||||
"We currently cannot reach the server. Please try again. Error code: {code}"
|
||||
msgstr ""
|
||||
"نحن في الوقت الراهن لا يمكن أن تصل إلى الخادم. حاول مرة اخرى. رمز الخطأ: "
|
||||
"{كود}"
|
||||
msgstr "لم نستطع معالجة طلبك، ولكننا نواصل المحاولة. رمز الخطأ : {code}"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:171
|
||||
msgid "We are processing your request …"
|
||||
msgstr "نحن معالجة طلبك ..."
|
||||
msgstr "نقوم بمعالجة طلبك …"
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:179
|
||||
msgid ""
|
||||
@@ -121,8 +115,8 @@ msgid ""
|
||||
"than one minute, please check your internet connection and then reload this "
|
||||
"page and try again."
|
||||
msgstr ""
|
||||
"نحن نرسل حاليا طلبك إلى الخادم. إذا كان هذا يأخذ دقيقة تعد من واحد، يرجى "
|
||||
"التحقق من اتصالك بالإنترنت ثم إعادة تحميل هذه الصفحة وحاول مرة أخرى."
|
||||
"يجري الآن معالجة طلبك، اذا أخذت العملية أكثر من دقيقة، يرجى التحقق من اتصالك "
|
||||
"بالإنترنت ثم حاول مرة أخرى."
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:216
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:34
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-12-05 13:33+0000\n"
|
||||
"PO-Revision-Date: 2019-11-22 10:01+0000\n"
|
||||
"Last-Translator: Carolina Fernández <cfermart@gmail.com>\n"
|
||||
"PO-Revision-Date: 2020-01-08 03:00+0000\n"
|
||||
"Last-Translator: oocf <oswaldocerna@gmail.com>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"es/>\n"
|
||||
"Language: es\n"
|
||||
@@ -3168,7 +3168,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/vouchers.py:178
|
||||
msgid "Specific seat"
|
||||
msgstr ""
|
||||
msgstr "Asiento especifico"
|
||||
|
||||
#: pretix/base/models/vouchers.py:182
|
||||
#: pretix/control/templates/pretixcontrol/vouchers/index.html:114
|
||||
@@ -5928,7 +5928,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py:105
|
||||
msgid "Grant access to team"
|
||||
msgstr ""
|
||||
msgstr "Dar acceso al equipo"
|
||||
|
||||
#: pretix/control/forms/event.py:106
|
||||
msgid ""
|
||||
@@ -6319,7 +6319,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py:518
|
||||
msgid "Social media image"
|
||||
msgstr ""
|
||||
msgstr "Imagen de redes sociales"
|
||||
|
||||
#: pretix/control/forms/event.py:521
|
||||
msgid ""
|
||||
@@ -19645,11 +19645,11 @@ msgstr "Italiano"
|
||||
|
||||
#: pretix/settings.py:413
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
msgstr "Ruso"
|
||||
|
||||
#: pretix/settings.py:414
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
msgstr "Letón"
|
||||
|
||||
#: pretix/settings.py:415
|
||||
msgid "Chinese (simplified)"
|
||||
|
||||
@@ -8,10 +8,10 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-12-05 13:34+0000\n"
|
||||
"PO-Revision-Date: 2019-09-13 18:00+0000\n"
|
||||
"Last-Translator: Gianmarco Palumbo <pal_gm@hotmail.it>\n"
|
||||
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/it/>\n"
|
||||
"PO-Revision-Date: 2019-12-20 19:00+0000\n"
|
||||
"Last-Translator: Patrick Arminio <patrick.arminio@gmail.com>\n"
|
||||
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix-js/it/>\n"
|
||||
"Language: it\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -160,11 +160,11 @@ msgstr "Oggetto testo"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:426
|
||||
msgid "Barcode area"
|
||||
msgstr ""
|
||||
msgstr "Area codice a barra"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:428
|
||||
msgid "Powered by pretix"
|
||||
msgstr ""
|
||||
msgstr "Realizzato con pretix"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:430
|
||||
msgid "Object"
|
||||
@@ -172,7 +172,7 @@ msgstr "Oggetto"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:434
|
||||
msgid "Ticket design"
|
||||
msgstr ""
|
||||
msgstr "Design biglietto"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/editor.js:687
|
||||
msgid "Saving failed."
|
||||
@@ -233,7 +233,7 @@ msgstr "Clicca per chiudere"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:749
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
msgstr "Hai cambiamenti non salvati!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/orderchange.js:24
|
||||
msgid "Calculating default price…"
|
||||
|
||||
@@ -7,10 +7,10 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-12-05 13:33+0000\n"
|
||||
"PO-Revision-Date: 2019-11-19 15:55+0000\n"
|
||||
"PO-Revision-Date: 2019-12-07 06:00+0000\n"
|
||||
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/"
|
||||
">\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
|
||||
"\n"
|
||||
"Language: nl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -191,10 +191,8 @@ msgid "Circular dependency between questions detected."
|
||||
msgstr "Circulaire afhankelijkheid tussen vragen gedetecteerd."
|
||||
|
||||
#: pretix/api/serializers/item.py:271 pretix/control/forms/item.py:86
|
||||
#, fuzzy
|
||||
#| msgid "This question will be asked during check-in."
|
||||
msgid "This type of question cannot be asked during check-in."
|
||||
msgstr "Deze vraag zal bij het inchecken worden gesteld."
|
||||
msgstr "Deze soort vraag kan niet bij het inchecken worden gesteld."
|
||||
|
||||
#: pretix/api/serializers/organizer.py:43 pretix/control/forms/organizer.py:363
|
||||
msgid ""
|
||||
@@ -2397,10 +2395,8 @@ msgid "Country code (ISO 3166-1 alpha-2)"
|
||||
msgstr "Landcode (ISO 3166-1 alpha-2)"
|
||||
|
||||
#: pretix/base/models/items.py:991
|
||||
#, fuzzy
|
||||
#| msgid "Line number"
|
||||
msgid "Phone number"
|
||||
msgstr "Regelnummer"
|
||||
msgstr "Telefoonnummer"
|
||||
|
||||
#: pretix/base/models/items.py:1002 pretix/base/models/items.py:1056
|
||||
#: pretix/control/forms/item.py:43
|
||||
@@ -3148,7 +3144,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/vouchers.py:178
|
||||
msgid "Specific seat"
|
||||
msgstr ""
|
||||
msgstr "Specifieke zitplaats"
|
||||
|
||||
#: pretix/base/models/vouchers.py:182
|
||||
#: pretix/control/templates/pretixcontrol/vouchers/index.html:114
|
||||
@@ -3216,12 +3212,12 @@ msgstr ""
|
||||
"maken."
|
||||
|
||||
#: pretix/base/models/vouchers.py:245 pretix/base/models/vouchers.py:338
|
||||
#, fuzzy
|
||||
#| msgid "You cannot select a quota and a specific product at the same time."
|
||||
msgid ""
|
||||
"You need to select a specific product or quota if this voucher should "
|
||||
"reserve tickets."
|
||||
msgstr "U kunt niet tegelijk een quotum en een specifiek product selecteren."
|
||||
msgstr ""
|
||||
"U moet een specifiek product of quotum selecteren als er tickets moeten "
|
||||
"worden gereserveerd voor deze voucher."
|
||||
|
||||
#: pretix/base/models/vouchers.py:255
|
||||
#, python-format
|
||||
@@ -3258,16 +3254,13 @@ msgid "A voucher with this code already exists."
|
||||
msgstr "Er bestaat al een voucher met deze code."
|
||||
|
||||
#: pretix/base/models/vouchers.py:355
|
||||
#, fuzzy
|
||||
#| msgid "You need to select a specific seat."
|
||||
msgid "You need to choose a date if you select a seat."
|
||||
msgstr "U moet een specifieke stoel kiezen."
|
||||
msgstr "U moet een datum kiezen als u een specifieke zitplaats selecteert."
|
||||
|
||||
#: pretix/base/models/vouchers.py:364
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "The selected date does not exist in this event series."
|
||||
#, python-brace-format
|
||||
msgid "The specified seat ID \"{id}\" does not exist for this event."
|
||||
msgstr "De geselecteerde datum bestaat niet in deze evenementenreeks."
|
||||
msgstr "De gekozen stoel met nummer \"{id}\" bestaat niet voor dit evenement."
|
||||
|
||||
#: pretix/base/models/vouchers.py:368
|
||||
#, python-brace-format
|
||||
@@ -3275,31 +3268,27 @@ msgid ""
|
||||
"The seat \"{id}\" is currently unavailable (blocked, already sold or a "
|
||||
"different voucher)."
|
||||
msgstr ""
|
||||
"De stoel \"{id}\" is momenteel niet beschikbaar (geblokkeerd, al verkocht of "
|
||||
"toegewezen aan een andere voucher)."
|
||||
|
||||
#: pretix/base/models/vouchers.py:373
|
||||
#, fuzzy
|
||||
#| msgid "You need to select a specific seat."
|
||||
msgid "You need to choose a specific product if you select a seat."
|
||||
msgstr "U moet een specifieke stoel kiezen."
|
||||
msgstr "U moet een specifiek product kiezen als u een stoel kiest."
|
||||
|
||||
#: pretix/base/models/vouchers.py:376
|
||||
#, fuzzy
|
||||
#| msgid "This gift card can only be used in test mode."
|
||||
msgid "Seat-specific vouchers can only be used once."
|
||||
msgstr "Deze cadeaubon kan alleen in de testmodus worden gebruikt."
|
||||
msgstr ""
|
||||
"Vouchers voor een specifieke stoel kunnen maar één keer worden gebruikt."
|
||||
|
||||
#: pretix/base/models/vouchers.py:379
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "You need to choose exactly one option from this category."
|
||||
#| msgid_plural "You need to choose %(min_count)s options from this category."
|
||||
#, python-brace-format
|
||||
msgid "You need to choose the product \"{prod}\" for this seat."
|
||||
msgstr "U moet precies één optie kiezen uit deze categorie."
|
||||
msgstr "U moet het product \"{prod}\" kiezen voor deze stoel."
|
||||
|
||||
#: pretix/base/models/vouchers.py:382
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "The identifier \"{}\" is already used for a different option."
|
||||
#, python-brace-format
|
||||
msgid "The seat \"{id}\" is already sold or currently blocked."
|
||||
msgstr "Het kenmerk \"{}\" wordt al voor een andere optie gebruikt."
|
||||
msgstr "De stoel \"{id}\" is al verkocht of geblokkeerd."
|
||||
|
||||
#: pretix/base/models/waitinglist.py:37
|
||||
msgid "On waiting list since"
|
||||
@@ -3581,12 +3570,14 @@ msgstr ""
|
||||
|
||||
#: pretix/base/payment.py:291
|
||||
msgid "Restrict to specific sales channels"
|
||||
msgstr ""
|
||||
msgstr "Beperken tot specifieke verkoopkanalen"
|
||||
|
||||
#: pretix/base/payment.py:299
|
||||
msgid ""
|
||||
"Only allow the usage of this payment provider in the following sales channels"
|
||||
msgstr ""
|
||||
"Sta het gebruik van deze betalingsprovider alleen toe voor de volgende "
|
||||
"verkoopkanalen"
|
||||
|
||||
#: pretix/base/payment.py:331
|
||||
msgctxt "invoice"
|
||||
@@ -3770,10 +3761,8 @@ msgid "Ticket code (barcode content)"
|
||||
msgstr "Ticket code (waarde van QR-code)"
|
||||
|
||||
#: pretix/base/pdf.py:57
|
||||
#, fuzzy
|
||||
#| msgid "Order position"
|
||||
msgid "Order position number"
|
||||
msgstr "Besteld product"
|
||||
msgstr "Plaatsnummer van bestelling"
|
||||
|
||||
#: pretix/base/pdf.py:62 pretix/control/forms/event.py:1563
|
||||
#: pretix/control/templates/pretixcontrol/items/index.html:33
|
||||
@@ -4179,11 +4168,8 @@ msgid "This voucher is not valid for this product."
|
||||
msgstr "Deze voucher is niet geldig voor dit product."
|
||||
|
||||
#: pretix/base/services/cart.py:84
|
||||
#, fuzzy
|
||||
#| msgctxt "subevent"
|
||||
#| msgid "This voucher is not valid for this event date."
|
||||
msgid "This voucher is not valid for this seat."
|
||||
msgstr "Deze voucher is niet geldig voor deze evenementsdatum."
|
||||
msgstr "Deze voucher is niet geldig voor deze stoel."
|
||||
|
||||
#: pretix/base/services/cart.py:86
|
||||
msgid "Your voucher is valid for a product that is currently not for sale."
|
||||
@@ -4681,28 +4667,22 @@ msgstr ""
|
||||
"contact op met de organisator van het evenement voor meer informatie."
|
||||
|
||||
#: pretix/base/services/seating.py:35 pretix/base/services/seating.py:86
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "You can not change the plan since seat \"{}\" is not present in the new "
|
||||
#| "plan and is already sold."
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You can not change the plan since seat \"%s\" is not present in the new plan "
|
||||
"and is already sold."
|
||||
msgstr ""
|
||||
"U kunt de plattegrond niet veranderen, omdat stoel \"{}\" niet aanwezig is "
|
||||
"U kunt de plattegrond niet veranderen, omdat stoel \"%s\" niet aanwezig is "
|
||||
"in de nieuwe plattegrond, en deze stoel al is verkocht."
|
||||
|
||||
#: pretix/base/services/seating.py:89
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "You can not change the plan since seat \"{}\" is not present in the new "
|
||||
#| "plan and is already sold."
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You can not change the plan since seat \"%s\" is not present in the new plan "
|
||||
"and is already used in a voucher."
|
||||
msgstr ""
|
||||
"U kunt de plattegrond niet veranderen, omdat stoel \"{}\" niet aanwezig is "
|
||||
"in de nieuwe plattegrond, en deze stoel al is verkocht."
|
||||
"U kunt de plattegrond niet veranderen, omdat stoel \"%s\" niet aanwezig is "
|
||||
"in de nieuwe plattegrond, en deze stoel al is gebruikt voor een voucher."
|
||||
|
||||
#: pretix/base/services/shredder.py:71
|
||||
msgid ""
|
||||
@@ -5791,7 +5771,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py:105
|
||||
msgid "Grant access to team"
|
||||
msgstr ""
|
||||
msgstr "Geef toegang aan team"
|
||||
|
||||
#: pretix/control/forms/event.py:106
|
||||
msgid ""
|
||||
@@ -5799,10 +5779,13 @@ msgid ""
|
||||
"have permission to edit all events under this organizer. Please select one "
|
||||
"of your existing teams that will be granted access to this event."
|
||||
msgstr ""
|
||||
"U kunt evenementen aanmaken voor deze organisator, maar u heeft geen "
|
||||
"toestemming om alle evenementen van deze organisator te bewerken. Geef één "
|
||||
"van de teams waar u deel van uitmaakt toegang tot dit evenement."
|
||||
|
||||
#: pretix/control/forms/event.py:111
|
||||
msgid "Create a new team for this event with me as the only member"
|
||||
msgstr ""
|
||||
msgstr "Maak een nieuw team voor dit evenement aan met mij als het enige lid"
|
||||
|
||||
#: pretix/control/forms/event.py:153 pretix/control/forms/event.py:291
|
||||
msgid ""
|
||||
@@ -6161,7 +6144,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py:518
|
||||
msgid "Social media image"
|
||||
msgstr ""
|
||||
msgstr "Social media-afbeelding"
|
||||
|
||||
#: pretix/control/forms/event.py:521
|
||||
msgid ""
|
||||
@@ -6171,10 +6154,17 @@ msgid ""
|
||||
"preview, so we recommend to make sure it still looks good only the center "
|
||||
"square is shown. If you do not fill this, we will use the logo given above."
|
||||
msgstr ""
|
||||
"Deze afbeelding zal worden gebruikt als u links naar uw ticketwinkel op "
|
||||
"sociale media plaatst. Facebook raadt aan om een afbeeldingsgrootte van 1200 "
|
||||
"bij 630 pixels te gebruiken, maar sommige platforms zoals WhatsApp en Reddit "
|
||||
"tonen alleen een vierkante voorvertoning. We raden aan om uw afbeelding zo "
|
||||
"te ontwerpen zodat hij er nog steeds goed uitziet als alleen het middelste "
|
||||
"vierkant wordt getoond. Als u hier geen afbeelding uploadt zullen we het "
|
||||
"logo dat hierboven is geüpload gebruiken."
|
||||
|
||||
#: pretix/control/forms/event.py:532
|
||||
msgid "Help text of the email field"
|
||||
msgstr ""
|
||||
msgstr "Helptekst van het e-mailveld"
|
||||
|
||||
#: pretix/control/forms/event.py:538
|
||||
msgid "End of presale text"
|
||||
@@ -7521,7 +7511,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/item.py:428
|
||||
msgid "Shown independently of other products"
|
||||
msgstr ""
|
||||
msgstr "Toon onafhankelijk van andere producten"
|
||||
|
||||
#: pretix/control/forms/item.py:507
|
||||
#, python-format
|
||||
@@ -7821,7 +7811,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/organizer.py:216
|
||||
msgid "Allow creating a new team during event creation"
|
||||
msgstr ""
|
||||
msgstr "Sta het aanmaken van nieuwe teams bij het aanmaken van evenementen toe"
|
||||
|
||||
#: pretix/control/forms/organizer.py:217
|
||||
msgid ""
|
||||
@@ -7830,6 +7820,11 @@ msgid ""
|
||||
"allows users to create an event-specified team on-the-fly, even when they do "
|
||||
"not have \"Can change teams and permissions\" permission."
|
||||
msgstr ""
|
||||
"Gebruikers die geen toegang hebben tot alle evenementen onder deze "
|
||||
"organisator moeten een van hun teams selecteren om toegang te geven aan hun "
|
||||
"aangemaakte evenement. Deze instelling staat gebruikers toe om een nieuw "
|
||||
"team aan te maken tijdens het aanmaken van een evenement, zelfs als de "
|
||||
"gebruikers niet de permissie \"Kan teams en machtigingen aanpassen\" hebben."
|
||||
|
||||
#: pretix/control/forms/organizer.py:244
|
||||
msgid "We strongly suggest to use a shade of red."
|
||||
@@ -7987,7 +7982,7 @@ msgstr "Uw wijzigingen konden niet worden opgeslagen. Zie onder voor details."
|
||||
|
||||
#: pretix/control/forms/vouchers.py:120
|
||||
msgid "Specific seat ID"
|
||||
msgstr ""
|
||||
msgstr "Specifiek stoelnummer"
|
||||
|
||||
#: pretix/control/forms/vouchers.py:155
|
||||
msgid "Invalid product selected."
|
||||
@@ -8080,7 +8075,7 @@ msgstr "Het aantal keren dat ELKE van deze vouchers kan worden gebruikt."
|
||||
|
||||
#: pretix/control/forms/vouchers.py:293
|
||||
msgid "Specific seat IDs"
|
||||
msgstr ""
|
||||
msgstr "Specifieke stoelnummers"
|
||||
|
||||
#: pretix/control/forms/vouchers.py:307
|
||||
msgid "CSV input needs to contain a header row in the first line."
|
||||
@@ -8126,10 +8121,8 @@ msgstr ""
|
||||
"U genereerde {codes} vouchers, maar gaf ontvangers voor {recp} vouchers op."
|
||||
|
||||
#: pretix/control/forms/vouchers.py:361
|
||||
#, fuzzy
|
||||
#| msgid "You need to specify either a quota or a product."
|
||||
msgid "You need to specify as many seats as voucher codes."
|
||||
msgstr "U moet een quotum of een product opgeven."
|
||||
msgstr "U moet evenveel stoelnummers als vouchercodes opgeven."
|
||||
|
||||
#: pretix/control/logdisplay.py:30
|
||||
msgid "The order has been changed:"
|
||||
@@ -8453,10 +8446,9 @@ msgid "Payment {local_id} has been canceled."
|
||||
msgstr "Betaling {local_id} is geannuleerd."
|
||||
|
||||
#: pretix/control/logdisplay.py:228
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "Payment {local_id} has failed."
|
||||
#, python-brace-format
|
||||
msgid "Cancelling payment {local_id} has failed."
|
||||
msgstr "Betaling {local_id} is mislukt."
|
||||
msgstr "Het annuleren van betaling {local_id} is mislukt."
|
||||
|
||||
#: pretix/control/logdisplay.py:229
|
||||
#, python-brace-format
|
||||
@@ -11106,16 +11098,13 @@ msgstr ""
|
||||
"het alleen binnen een bepaalde tijd beschikbaar moet zijn."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/item/base.html:29
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "This product is currently not being sold since you configured below that "
|
||||
#| "it should only be available in a certain timeframe."
|
||||
msgid ""
|
||||
"This product is currently not being shown since you configured below that it "
|
||||
"should only be visible if a certain other quota is already sold out."
|
||||
msgstr ""
|
||||
"Dit product is momenteel niet te koop, omdat u hieronder heeft ingesteld dat "
|
||||
"het alleen binnen een bepaalde tijd beschikbaar moet zijn."
|
||||
"Dit product wordt momenteel niet getoond, omdat u hieronder heeft ingesteld "
|
||||
"dat het product alleen moet worden getoond wanneer een bepaald ander quotum "
|
||||
"al is uitverkocht."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/item/create.html:23
|
||||
msgid "Quota settings"
|
||||
@@ -11748,7 +11737,7 @@ msgstr "Beheer uw eigen applicaties"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/oauth/authorized.html:18
|
||||
msgid "Permissions"
|
||||
msgstr "Rechten"
|
||||
msgstr "Permissies"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/oauth/authorized.html:59
|
||||
msgid "No applications have access to your pretix account."
|
||||
@@ -12429,12 +12418,11 @@ msgid "Create a new gift card"
|
||||
msgstr "Nieuwe cadeaubon aanmaken"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:103
|
||||
#, fuzzy
|
||||
#| msgid "This gift card is not accepted by this event organizer."
|
||||
msgid ""
|
||||
"The gift card can be used to buy tickets for all events of this organizer."
|
||||
msgstr ""
|
||||
"Deze cadeaubon wordt niet geaccepteerd door de organisator van dit evenement."
|
||||
"Deze cadeaubon kan worden gebruikt om tickets te kopen voor alle evenementen "
|
||||
"van deze organisator."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:111
|
||||
msgid "Manual refund"
|
||||
@@ -15088,17 +15076,15 @@ msgstr ""
|
||||
"kunt de terugbetaling hieronder als voltooid aanmerken."
|
||||
|
||||
#: pretix/control/views/orders.py:848
|
||||
#, fuzzy
|
||||
#| msgid "The gift card has been created and can now be used."
|
||||
msgid ""
|
||||
"A new gift card was created. You can now send the user their gift card code."
|
||||
msgstr "De cadeaubon is aangemaakt en kan nu worden gebruikt."
|
||||
msgstr ""
|
||||
"De cadeaubon is aangemaakt. U kunt de cadeauboncode nu naar de gebruiker "
|
||||
"sturen."
|
||||
|
||||
#: pretix/control/views/orders.py:855
|
||||
#, fuzzy
|
||||
#| msgid "Gift card code"
|
||||
msgid "Your gift card code"
|
||||
msgstr "Cadeauboncode"
|
||||
msgstr "Uw cadeauboncode"
|
||||
|
||||
#: pretix/control/views/orders.py:856
|
||||
#, python-brace-format
|
||||
@@ -15112,6 +15098,14 @@ msgid ""
|
||||
"\n"
|
||||
"Your {event} team"
|
||||
msgstr ""
|
||||
"Hallo,\n"
|
||||
"\n"
|
||||
"We hebben u {amount} terugbetaald voor uw bestelling.\n"
|
||||
"\n"
|
||||
"U kunt de cadeauboncode {giftcard} gebruiken om te betalen voor toekomstige "
|
||||
"bestellingen in onze winkel.\n"
|
||||
"\n"
|
||||
"De organisatie van {event}"
|
||||
|
||||
#: pretix/control/views/orders.py:866
|
||||
msgid "The refunds you selected do not match the selected total refund amount."
|
||||
@@ -16251,6 +16245,8 @@ msgid ""
|
||||
"Negative amount but refund can't be logged, please create manual refund "
|
||||
"first."
|
||||
msgstr ""
|
||||
"Negatief bedrag maar terugbetaling kan niet worden opgeslagen, maak eerst "
|
||||
"een handmatige terugbetaling aan."
|
||||
|
||||
#: pretix/plugins/banktransfer/views.py:90
|
||||
msgid "The order is already marked as paid."
|
||||
@@ -19018,11 +19014,9 @@ msgstr ""
|
||||
"weer tickets beschikbaar zijn."
|
||||
|
||||
#: pretix/presale/views/widget.py:243
|
||||
#, fuzzy
|
||||
#| msgid "<a %(a_attr)s>event ticketing powered by pretix</a>"
|
||||
msgctxt "widget"
|
||||
msgid "event ticketing powered by pretix"
|
||||
msgstr "<a %(a_attr)s>ticketverkoop mogelijk gemaakt door pretix</a>"
|
||||
msgstr "ticketverkoop mogelijk gemaakt door pretix"
|
||||
|
||||
#: pretix/presale/views/widget.py:258
|
||||
msgid "This ticket shop is currently disabled."
|
||||
@@ -19095,11 +19089,11 @@ msgstr "Italiaans"
|
||||
|
||||
#: pretix/settings.py:413
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
msgstr "Russisch"
|
||||
|
||||
#: pretix/settings.py:414
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
msgstr "Lets"
|
||||
|
||||
#: pretix/settings.py:415
|
||||
msgid "Chinese (simplified)"
|
||||
|
||||
@@ -7,7 +7,7 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-12-05 13:34+0000\n"
|
||||
"PO-Revision-Date: 2019-08-03 22:00+0000\n"
|
||||
"PO-Revision-Date: 2019-12-07 06:00+0000\n"
|
||||
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"nl/>\n"
|
||||
@@ -96,8 +96,6 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
#, fuzzy
|
||||
#| msgid "The request took to long. Please try again."
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "De aanvraag duurde te lang, probeer het alstublieft opnieuw."
|
||||
|
||||
@@ -231,7 +229,7 @@ msgstr "Klik om te sluiten"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:749
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
msgstr "U heeft nog niet opgeslagen wijzigingen!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/orderchange.js:24
|
||||
msgid "Calculating default price…"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-12-05 13:33+0000\n"
|
||||
"PO-Revision-Date: 2019-11-19 15:55+0000\n"
|
||||
"PO-Revision-Date: 2020-01-09 22:00+0000\n"
|
||||
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
|
||||
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix/nl_Informal/>\n"
|
||||
@@ -192,10 +192,8 @@ msgid "Circular dependency between questions detected."
|
||||
msgstr "Kringafhankelijkheid tussen vragen gedetecteerd."
|
||||
|
||||
#: pretix/api/serializers/item.py:271 pretix/control/forms/item.py:86
|
||||
#, fuzzy
|
||||
#| msgid "This question will be asked during check-in."
|
||||
msgid "This type of question cannot be asked during check-in."
|
||||
msgstr "Deze vraag zal bij het inchecken worden gesteld."
|
||||
msgstr "Deze soort vraag kan niet bij het inchecken worden gesteld."
|
||||
|
||||
#: pretix/api/serializers/organizer.py:43 pretix/control/forms/organizer.py:363
|
||||
msgid ""
|
||||
@@ -2398,10 +2396,8 @@ msgid "Country code (ISO 3166-1 alpha-2)"
|
||||
msgstr "Landcode (ISO 3166-1 alpha-2)"
|
||||
|
||||
#: pretix/base/models/items.py:991
|
||||
#, fuzzy
|
||||
#| msgid "Line number"
|
||||
msgid "Phone number"
|
||||
msgstr "Regelnummer"
|
||||
msgstr "Telefoonnummer"
|
||||
|
||||
#: pretix/base/models/items.py:1002 pretix/base/models/items.py:1056
|
||||
#: pretix/control/forms/item.py:43
|
||||
@@ -3149,7 +3145,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/vouchers.py:178
|
||||
msgid "Specific seat"
|
||||
msgstr ""
|
||||
msgstr "Specifieke zitplaats"
|
||||
|
||||
#: pretix/base/models/vouchers.py:182
|
||||
#: pretix/control/templates/pretixcontrol/vouchers/index.html:114
|
||||
@@ -3216,12 +3212,12 @@ msgstr ""
|
||||
"Het is op dit moment niet mogelijk om vouchers voor add-onproducten te maken."
|
||||
|
||||
#: pretix/base/models/vouchers.py:245 pretix/base/models/vouchers.py:338
|
||||
#, fuzzy
|
||||
#| msgid "You cannot select a quota and a specific product at the same time."
|
||||
msgid ""
|
||||
"You need to select a specific product or quota if this voucher should "
|
||||
"reserve tickets."
|
||||
msgstr "Je kan niet tegelijk een quotum en een specifiek product selecteren."
|
||||
msgstr ""
|
||||
"Je moet een specifiek product of quotum kiezen als er kaartjes moeten worden "
|
||||
"gereserveerd voor deze voucher."
|
||||
|
||||
#: pretix/base/models/vouchers.py:255
|
||||
#, python-format
|
||||
@@ -3258,16 +3254,13 @@ msgid "A voucher with this code already exists."
|
||||
msgstr "Er bestaat al een voucher met deze code."
|
||||
|
||||
#: pretix/base/models/vouchers.py:355
|
||||
#, fuzzy
|
||||
#| msgid "You need to select a specific seat."
|
||||
msgid "You need to choose a date if you select a seat."
|
||||
msgstr "Je moet een specifieke stoel kiezen."
|
||||
msgstr "Je moet een datum kiezen als je een stoel kiest."
|
||||
|
||||
#: pretix/base/models/vouchers.py:364
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "The selected date does not exist in this event series."
|
||||
#, python-brace-format
|
||||
msgid "The specified seat ID \"{id}\" does not exist for this event."
|
||||
msgstr "De geselecteerde datum bestaat niet in deze evenementenreeks."
|
||||
msgstr "De gekozen stoel met nummer \"{id}\" bestaat niet voor dit evenement."
|
||||
|
||||
#: pretix/base/models/vouchers.py:368
|
||||
#, python-brace-format
|
||||
@@ -3275,31 +3268,27 @@ msgid ""
|
||||
"The seat \"{id}\" is currently unavailable (blocked, already sold or a "
|
||||
"different voucher)."
|
||||
msgstr ""
|
||||
"De stoel \"{id}\" is momenteel niet beschikbaar (geblokkeerd, al verkocht of "
|
||||
"toegewezen aan een andere voucher)."
|
||||
|
||||
#: pretix/base/models/vouchers.py:373
|
||||
#, fuzzy
|
||||
#| msgid "You need to select a specific seat."
|
||||
msgid "You need to choose a specific product if you select a seat."
|
||||
msgstr "Je moet een specifieke stoel kiezen."
|
||||
msgstr "Je moet een specifiek product kiezen als je een stoel kiest."
|
||||
|
||||
#: pretix/base/models/vouchers.py:376
|
||||
#, fuzzy
|
||||
#| msgid "This gift card can only be used in test mode."
|
||||
msgid "Seat-specific vouchers can only be used once."
|
||||
msgstr "Deze cadeaubon kan alleen in de testmodus worden gebruikt."
|
||||
msgstr ""
|
||||
"Vouchers voor een specifieke stoel kunnen maar één keer worden gebruikt."
|
||||
|
||||
#: pretix/base/models/vouchers.py:379
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "You need to choose exactly one option from this category."
|
||||
#| msgid_plural "You need to choose %(min_count)s options from this category."
|
||||
#, python-brace-format
|
||||
msgid "You need to choose the product \"{prod}\" for this seat."
|
||||
msgstr "Je moet precies één optie kiezen uit deze categorie."
|
||||
msgstr "Je moet het product \"{prod}\" kiezen voor deze stoel."
|
||||
|
||||
#: pretix/base/models/vouchers.py:382
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "The identifier \"{}\" is already used for a different option."
|
||||
#, python-brace-format
|
||||
msgid "The seat \"{id}\" is already sold or currently blocked."
|
||||
msgstr "Het kenmerk \"{}\" wordt al voor een andere optie gebruikt."
|
||||
msgstr "De stoel \"{id}\" is al verkocht of geblokkeerd."
|
||||
|
||||
#: pretix/base/models/waitinglist.py:37
|
||||
msgid "On waiting list since"
|
||||
@@ -3581,12 +3570,14 @@ msgstr ""
|
||||
|
||||
#: pretix/base/payment.py:291
|
||||
msgid "Restrict to specific sales channels"
|
||||
msgstr ""
|
||||
msgstr "Beperken tot specifieke verkoopkanalen"
|
||||
|
||||
#: pretix/base/payment.py:299
|
||||
msgid ""
|
||||
"Only allow the usage of this payment provider in the following sales channels"
|
||||
msgstr ""
|
||||
"Sta het gebruik van deze betalingsprovider alleen toe voor de volgende "
|
||||
"verkoopkanalen"
|
||||
|
||||
#: pretix/base/payment.py:331
|
||||
msgctxt "invoice"
|
||||
@@ -3770,10 +3761,8 @@ msgid "Ticket code (barcode content)"
|
||||
msgstr "Kaartjescode (waarde van QR-code)"
|
||||
|
||||
#: pretix/base/pdf.py:57
|
||||
#, fuzzy
|
||||
#| msgid "Order position"
|
||||
msgid "Order position number"
|
||||
msgstr "Besteld product"
|
||||
msgstr "Plaatsnummer van bestelling"
|
||||
|
||||
#: pretix/base/pdf.py:62 pretix/control/forms/event.py:1563
|
||||
#: pretix/control/templates/pretixcontrol/items/index.html:33
|
||||
@@ -4179,11 +4168,8 @@ msgid "This voucher is not valid for this product."
|
||||
msgstr "Deze voucher is niet geldig voor dit product."
|
||||
|
||||
#: pretix/base/services/cart.py:84
|
||||
#, fuzzy
|
||||
#| msgctxt "subevent"
|
||||
#| msgid "This voucher is not valid for this event date."
|
||||
msgid "This voucher is not valid for this seat."
|
||||
msgstr "Deze voucher is niet geldig voor deze evenementsdatum."
|
||||
msgstr "Deze voucher is niet geldig voor deze stoel."
|
||||
|
||||
#: pretix/base/services/cart.py:86
|
||||
msgid "Your voucher is valid for a product that is currently not for sale."
|
||||
@@ -4680,28 +4666,22 @@ msgstr ""
|
||||
"contact op met de organisator van het evenement voor meer informatie."
|
||||
|
||||
#: pretix/base/services/seating.py:35 pretix/base/services/seating.py:86
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "You can not change the plan since seat \"{}\" is not present in the new "
|
||||
#| "plan and is already sold."
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You can not change the plan since seat \"%s\" is not present in the new plan "
|
||||
"and is already sold."
|
||||
msgstr ""
|
||||
"Je kan de plattegrond niet veranderen, omdat stoel \"{}\" niet aanwezig is "
|
||||
"Je kan de plattegrond niet veranderen, omdat stoel \"%s\" niet aanwezig is "
|
||||
"in de nieuwe plattegrond, en deze stoel al is verkocht."
|
||||
|
||||
#: pretix/base/services/seating.py:89
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "You can not change the plan since seat \"{}\" is not present in the new "
|
||||
#| "plan and is already sold."
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You can not change the plan since seat \"%s\" is not present in the new plan "
|
||||
"and is already used in a voucher."
|
||||
msgstr ""
|
||||
"Je kan de plattegrond niet veranderen, omdat stoel \"{}\" niet aanwezig is "
|
||||
"in de nieuwe plattegrond, en deze stoel al is verkocht."
|
||||
"Je kan de plattegrond niet veranderen, omdat stoel \"%s\" niet aanwezig is "
|
||||
"in de nieuwe plattegrond, en deze stoel al is gebruikt voor een voucher."
|
||||
|
||||
#: pretix/base/services/shredder.py:71
|
||||
msgid ""
|
||||
@@ -5781,7 +5761,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py:105
|
||||
msgid "Grant access to team"
|
||||
msgstr ""
|
||||
msgstr "Geef toegang aan team"
|
||||
|
||||
#: pretix/control/forms/event.py:106
|
||||
msgid ""
|
||||
@@ -5789,10 +5769,13 @@ msgid ""
|
||||
"have permission to edit all events under this organizer. Please select one "
|
||||
"of your existing teams that will be granted access to this event."
|
||||
msgstr ""
|
||||
"Je kan evenementen aanmaken voor deze organisator, maar je hebt geen "
|
||||
"toestemming om alle evenementen van deze organisator te bewerken. Geef één "
|
||||
"van de teams waar je in zit toegang tot dit evenement."
|
||||
|
||||
#: pretix/control/forms/event.py:111
|
||||
msgid "Create a new team for this event with me as the only member"
|
||||
msgstr ""
|
||||
msgstr "Maak een nieuw team voor dit evenement aan met mij als het enige lid"
|
||||
|
||||
#: pretix/control/forms/event.py:153 pretix/control/forms/event.py:291
|
||||
msgid ""
|
||||
@@ -6152,7 +6135,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py:518
|
||||
msgid "Social media image"
|
||||
msgstr ""
|
||||
msgstr "Social media-afbeelding"
|
||||
|
||||
#: pretix/control/forms/event.py:521
|
||||
msgid ""
|
||||
@@ -6162,10 +6145,17 @@ msgid ""
|
||||
"preview, so we recommend to make sure it still looks good only the center "
|
||||
"square is shown. If you do not fill this, we will use the logo given above."
|
||||
msgstr ""
|
||||
"Deze afbeelding zal als voorvertoning worden gebruikt als je links naar je "
|
||||
"kaartjeswinkel op sociale media plaatst. Facebook raadt aan om een "
|
||||
"afbeeldingsgrootte van 1200 bij 630 pixels te gebruiken, maar sommige "
|
||||
"platforms zoals WhatsApp en Reddit tonen alleen een vierkante voorvertoning. "
|
||||
"We raden aan om je afbeelding zo te ontwerpen zodat hij er nog steeds goed "
|
||||
"uitziet als alleen het middelste vierkant wordt getoond. Als je hier geen "
|
||||
"afbeelding uploadt zullen we het logo dat hierboven is geüpload gebruiken."
|
||||
|
||||
#: pretix/control/forms/event.py:532
|
||||
msgid "Help text of the email field"
|
||||
msgstr ""
|
||||
msgstr "Helptekst van het e-mailveld"
|
||||
|
||||
#: pretix/control/forms/event.py:538
|
||||
msgid "End of presale text"
|
||||
@@ -7512,7 +7502,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/item.py:428
|
||||
msgid "Shown independently of other products"
|
||||
msgstr ""
|
||||
msgstr "Toon onafhankelijk van andere producten"
|
||||
|
||||
#: pretix/control/forms/item.py:507
|
||||
#, python-format
|
||||
@@ -7811,7 +7801,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/forms/organizer.py:216
|
||||
msgid "Allow creating a new team during event creation"
|
||||
msgstr ""
|
||||
msgstr "Sta het aanmaken van nieuwe teams bij het aanmaken van evenementen toe"
|
||||
|
||||
#: pretix/control/forms/organizer.py:217
|
||||
msgid ""
|
||||
@@ -7820,6 +7810,11 @@ msgid ""
|
||||
"allows users to create an event-specified team on-the-fly, even when they do "
|
||||
"not have \"Can change teams and permissions\" permission."
|
||||
msgstr ""
|
||||
"Gebruikers die geen toegang hebben tot alle evenementen onder deze "
|
||||
"organisator moeten een van hun teams selecteren om toegang te geven aan hun "
|
||||
"aangemaakte evenement. Deze instelling staat gebruikers toe om een nieuw "
|
||||
"team aan te maken tijdens het aanmaken van een evenement, zelfs als de "
|
||||
"gebruikers niet de permissie \"Kan teams en machtigingen aanpassen\" hebben."
|
||||
|
||||
#: pretix/control/forms/organizer.py:244
|
||||
msgid "We strongly suggest to use a shade of red."
|
||||
@@ -7977,7 +7972,7 @@ msgstr "Je wijzigingen konden niet worden opgeslagen. Zie onder voor details."
|
||||
|
||||
#: pretix/control/forms/vouchers.py:120
|
||||
msgid "Specific seat ID"
|
||||
msgstr ""
|
||||
msgstr "Specifiek stoelnummer"
|
||||
|
||||
#: pretix/control/forms/vouchers.py:155
|
||||
msgid "Invalid product selected."
|
||||
@@ -8070,7 +8065,7 @@ msgstr "Het aantal keren dat ELKE van deze vouchers kan worden gebruikt."
|
||||
|
||||
#: pretix/control/forms/vouchers.py:293
|
||||
msgid "Specific seat IDs"
|
||||
msgstr ""
|
||||
msgstr "Specifieke stoelnummers"
|
||||
|
||||
#: pretix/control/forms/vouchers.py:307
|
||||
msgid "CSV input needs to contain a header row in the first line."
|
||||
@@ -8115,10 +8110,8 @@ msgid ""
|
||||
msgstr "Je genereerde {codes} vouchers, maar hebt {recp} ontvangers opgegeven."
|
||||
|
||||
#: pretix/control/forms/vouchers.py:361
|
||||
#, fuzzy
|
||||
#| msgid "You need to specify either a quota or a product."
|
||||
msgid "You need to specify as many seats as voucher codes."
|
||||
msgstr "Je moet een quotum of een product opgeven."
|
||||
msgstr "Je moet evenveel stoelnummers als vouchercodes opgeven."
|
||||
|
||||
#: pretix/control/logdisplay.py:30
|
||||
msgid "The order has been changed:"
|
||||
@@ -8442,10 +8435,9 @@ msgid "Payment {local_id} has been canceled."
|
||||
msgstr "Betaling {local_id} is geannuleerd."
|
||||
|
||||
#: pretix/control/logdisplay.py:228
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "Payment {local_id} has failed."
|
||||
#, python-brace-format
|
||||
msgid "Cancelling payment {local_id} has failed."
|
||||
msgstr "Betaling {local_id} is mislukt."
|
||||
msgstr "Het annuleren van betaling {local_id} is mislukt."
|
||||
|
||||
#: pretix/control/logdisplay.py:229
|
||||
#, python-brace-format
|
||||
@@ -11096,16 +11088,13 @@ msgstr ""
|
||||
"het alleen binnen een bepaalde tijd beschikbaar moet zijn."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/item/base.html:29
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "This product is currently not being sold since you configured below that "
|
||||
#| "it should only be available in a certain timeframe."
|
||||
msgid ""
|
||||
"This product is currently not being shown since you configured below that it "
|
||||
"should only be visible if a certain other quota is already sold out."
|
||||
msgstr ""
|
||||
"Dit product is momenteel niet te koop, omdat je hieronder hebt ingesteld dat "
|
||||
"het alleen binnen een bepaalde tijd beschikbaar moet zijn."
|
||||
"Dit product wordt momenteel niet getoond, omdat je hieronder hebt ingesteld "
|
||||
"dat het product alleen moet worden getoond wanneer een bepaald ander quotum "
|
||||
"al is uitverkocht."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/item/create.html:23
|
||||
msgid "Quota settings"
|
||||
@@ -11739,7 +11728,7 @@ msgstr "Beheer je eigen applicaties"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/oauth/authorized.html:18
|
||||
msgid "Permissions"
|
||||
msgstr "Rechten"
|
||||
msgstr "Permissies"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/oauth/authorized.html:59
|
||||
msgid "No applications have access to your pretix account."
|
||||
@@ -12420,12 +12409,11 @@ msgid "Create a new gift card"
|
||||
msgstr "Nieuwe cadeaubon aanmaken"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:103
|
||||
#, fuzzy
|
||||
#| msgid "This gift card is not accepted by this event organizer."
|
||||
msgid ""
|
||||
"The gift card can be used to buy tickets for all events of this organizer."
|
||||
msgstr ""
|
||||
"Deze cadeaubon wordt niet geaccepteerd door de organisator van dit evenement."
|
||||
"Deze cadeaubon kan worden gebruikt om kaartjes te kopen voor alle "
|
||||
"evenementen van deze organisator."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:111
|
||||
msgid "Manual refund"
|
||||
@@ -15082,17 +15070,15 @@ msgstr ""
|
||||
"Je kan de terugbetaling hieronder als voltooid aanmerken."
|
||||
|
||||
#: pretix/control/views/orders.py:848
|
||||
#, fuzzy
|
||||
#| msgid "The gift card has been created and can now be used."
|
||||
msgid ""
|
||||
"A new gift card was created. You can now send the user their gift card code."
|
||||
msgstr "De cadeaubon is aangemaakt en kan nu worden gebruikt."
|
||||
msgstr ""
|
||||
"De cadeaubon is aangemaakt. Je kan de cadeauboncode nu naar de gebruiker "
|
||||
"sturen."
|
||||
|
||||
#: pretix/control/views/orders.py:855
|
||||
#, fuzzy
|
||||
#| msgid "Gift card code"
|
||||
msgid "Your gift card code"
|
||||
msgstr "Cadeauboncode"
|
||||
msgstr "Je cadeauboncode"
|
||||
|
||||
#: pretix/control/views/orders.py:856
|
||||
#, python-brace-format
|
||||
@@ -15106,6 +15092,14 @@ msgid ""
|
||||
"\n"
|
||||
"Your {event} team"
|
||||
msgstr ""
|
||||
"Hallo,\n"
|
||||
"\n"
|
||||
"We hebben je {amount} voor je bestelling terugbetaald.\n"
|
||||
"\n"
|
||||
"Je kan de cadeauboncode {giftcard} gebruiken om te betalen voor toekomstige "
|
||||
"bestellingen in onze winkel.\n"
|
||||
"\n"
|
||||
"De organisatie van {event}"
|
||||
|
||||
#: pretix/control/views/orders.py:866
|
||||
msgid "The refunds you selected do not match the selected total refund amount."
|
||||
@@ -16245,6 +16239,8 @@ msgid ""
|
||||
"Negative amount but refund can't be logged, please create manual refund "
|
||||
"first."
|
||||
msgstr ""
|
||||
"Negatief bedrag maar terugbetaling kan niet worden opgeslagen, maak eerst "
|
||||
"een handmatige terugbetaling aan."
|
||||
|
||||
#: pretix/plugins/banktransfer/views.py:90
|
||||
msgid "The order is already marked as paid."
|
||||
@@ -19014,11 +19010,9 @@ msgstr ""
|
||||
"weer kaartjes beschikbaar zijn."
|
||||
|
||||
#: pretix/presale/views/widget.py:243
|
||||
#, fuzzy
|
||||
#| msgid "<a %(a_attr)s>event ticketing powered by pretix</a>"
|
||||
msgctxt "widget"
|
||||
msgid "event ticketing powered by pretix"
|
||||
msgstr "<a %(a_attr)s>kaartverkoop mogelijk gemaakt door pretix</a>"
|
||||
msgstr "kaartverkoop mogelijk gemaakt door pretix"
|
||||
|
||||
#: pretix/presale/views/widget.py:258
|
||||
msgid "This ticket shop is currently disabled."
|
||||
@@ -19091,11 +19085,11 @@ msgstr "Italiaans"
|
||||
|
||||
#: pretix/settings.py:413
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
msgstr "Russisch"
|
||||
|
||||
#: pretix/settings.py:414
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
msgstr "Lets"
|
||||
|
||||
#: pretix/settings.py:415
|
||||
msgid "Chinese (simplified)"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-12-05 13:34+0000\n"
|
||||
"PO-Revision-Date: 2019-08-03 22:00+0000\n"
|
||||
"PO-Revision-Date: 2019-12-16 04:00+0000\n"
|
||||
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
|
||||
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix-js/nl_Informal/>\n"
|
||||
@@ -97,8 +97,6 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:125
|
||||
#: pretix/static/pretixcontrol/js/ui/mail.js:21
|
||||
#, fuzzy
|
||||
#| msgid "The request took to long. Please try again."
|
||||
msgid "The request took too long. Please try again."
|
||||
msgstr "De aanvraag duurde te lang, probeer het alsjeblieft opnieuw."
|
||||
|
||||
@@ -233,7 +231,7 @@ msgstr "Klik om te sluiten"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:749
|
||||
msgid "You have unsaved changes!"
|
||||
msgstr ""
|
||||
msgstr "Je hebt nog niet opgeslagen wijzigingen!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/orderchange.js:24
|
||||
msgid "Calculating default price…"
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Import bank data" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Import bank data" %}</h1>
|
||||
<h1>
|
||||
{% trans "Import bank data" %}
|
||||
{% if runningimport %}
|
||||
<small>{% trans "Import currently running…" %}</small>
|
||||
{% else %}
|
||||
<small>{% blocktrans trimmed with date=lastimport.created|date:"SHORT_DATETIME_FORMAT" %}Last import: {{ date }}{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% block inner %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,7 +3,16 @@
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Import bank data" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Import bank data" %}</h1>
|
||||
<h1>
|
||||
{% trans "Import bank data" %}
|
||||
{% if runningimport %}
|
||||
<small>{% trans "Import currently running…" %}</small>
|
||||
{% else %}
|
||||
<small>{% blocktrans trimmed with date=lastimport.created|date:"SHORT_DATETIME_FORMAT" %}Last import:
|
||||
{{ date }}{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
{% block inner %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -481,7 +481,25 @@ class ImportView(ListView):
|
||||
if not self.request.event.has_subevents and self.request.event.settings.get('payment_term_last'):
|
||||
if now() > self.request.event.payment_term_last:
|
||||
ctx['no_more_payments'] = True
|
||||
ctx['lastimport'] = BankImportJob.objects.filter(
|
||||
state=BankImportJob.STATE_COMPLETED,
|
||||
organizer=self.request.organizer,
|
||||
event=self.request.event
|
||||
).order_by('created').last()
|
||||
ctx['runningimport'] = BankImportJob.objects.filter(
|
||||
state__in=[BankImportJob.STATE_PENDING, BankImportJob.STATE_RUNNING],
|
||||
event=self.request.event
|
||||
).order_by('created').last()
|
||||
else:
|
||||
ctx['lastimport'] = BankImportJob.objects.filter(
|
||||
state=BankImportJob.STATE_COMPLETED,
|
||||
organizer=self.request.organizer,
|
||||
event__isnull=True
|
||||
).order_by('created').last()
|
||||
ctx['runningimport'] = BankImportJob.objects.filter(
|
||||
state__in=[BankImportJob.STATE_PENDING, BankImportJob.STATE_RUNNING],
|
||||
event__isnull=True
|
||||
).order_by('created').last()
|
||||
ctx['basetpl'] = 'pretixplugins/banktransfer/import_base_organizer.html'
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@@ -3,7 +3,9 @@ from collections import OrderedDict
|
||||
import dateutil.parser
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db.models import Case, Exists, Max, OuterRef, Subquery, Value, When
|
||||
from django.db.models import (
|
||||
Case, Exists, Max, OuterRef, Q, Subquery, Value, When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, NullIf
|
||||
from django.urls import reverse
|
||||
from django.utils.formats import date_format
|
||||
@@ -44,6 +46,11 @@ class CheckInListMixin(BaseExporter):
|
||||
label=_('Include QR-code secret'),
|
||||
required=False
|
||||
)),
|
||||
('attention_only',
|
||||
forms.BooleanField(
|
||||
label=_('Only tickets requiring special attention'),
|
||||
required=False
|
||||
)),
|
||||
('sort',
|
||||
forms.ChoiceField(
|
||||
label=_('Sort by'),
|
||||
@@ -136,6 +143,9 @@ class CheckInListMixin(BaseExporter):
|
||||
'resolved_name_part'
|
||||
)
|
||||
|
||||
if form_data.get('attention_only'):
|
||||
qs = qs.filter(Q(item__checkin_attention=True) | Q(order__checkin_attention=True))
|
||||
|
||||
if not cl.include_pending:
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID)
|
||||
else:
|
||||
|
||||
@@ -513,9 +513,12 @@ class OrderTaxListReport(ListExporter):
|
||||
)
|
||||
tax_rates = sorted(tax_rates)
|
||||
|
||||
yield [
|
||||
_('Order code'), _('Order date'), _('Status'), _('Payment date'), _('Order total'),
|
||||
headers = [
|
||||
_('Order code'), _('Order date'),
|
||||
_('Company'), _('Name'),
|
||||
_('Country'), _('VAT ID'), _('Status'), _('Payment date'), _('Order total'),
|
||||
] + sum(([str(t) + ' % ' + _('Gross'), str(t) + ' % ' + _('Tax')] for t in tax_rates), [])
|
||||
yield headers
|
||||
|
||||
op_date = OrderPayment.objects.filter(
|
||||
order=OuterRef('order'),
|
||||
@@ -531,7 +534,8 @@ class OrderTaxListReport(ListExporter):
|
||||
order__event=self.event,
|
||||
).annotate(payment_date=Subquery(op_date, output_field=models.DateTimeField())).values(
|
||||
'order__code', 'order__datetime', 'payment_date', 'order__total', 'tax_rate', 'order__status',
|
||||
'order__id'
|
||||
'order__id', 'order__invoice_address__name_cached', 'order__invoice_address__company',
|
||||
'order__invoice_address__country', 'order__invoice_address__vat_id'
|
||||
).annotate(prices=Sum('price'), tax_values=Sum('tax_value')).order_by(
|
||||
'order__datetime' if form_data['sort'] == 'datetime' else 'payment_date',
|
||||
'order__datetime',
|
||||
@@ -557,6 +561,10 @@ class OrderTaxListReport(ListExporter):
|
||||
row = [
|
||||
op['order__code'],
|
||||
date_format(op['order__datetime'].astimezone(tz), "SHORT_DATE_FORMAT"),
|
||||
op['order__invoice_address__company'],
|
||||
op['order__invoice_address__name_cached'],
|
||||
op['order__invoice_address__country'],
|
||||
op['order__invoice_address__vat_id'],
|
||||
status_labels[op['order__status']],
|
||||
date_format(op['payment_date'], "SHORT_DATE_FORMAT") if op['payment_date'] else '',
|
||||
round_decimal(op['order__total'], self.event.currency),
|
||||
@@ -565,21 +573,21 @@ class OrderTaxListReport(ListExporter):
|
||||
for i, rate in enumerate(tax_rates):
|
||||
odata = fee_sum_cache.get((op['order__id'], rate))
|
||||
if odata:
|
||||
row[5 + 2 * i] = odata['grosssum'] or 0
|
||||
row[6 + 2 * i] = odata['taxsum'] or 0
|
||||
row[9 + 2 * i] = odata['grosssum'] or 0
|
||||
row[10 + 2 * i] = odata['taxsum'] or 0
|
||||
tax_sums[rate] += odata['taxsum'] or 0
|
||||
price_sums[rate] += odata['grosssum'] or 0
|
||||
|
||||
i = tax_rates.index(op['tax_rate'])
|
||||
row[5 + 2 * i] = round_decimal(row[5 + 2 * i] + op['prices'], self.event.currency)
|
||||
row[6 + 2 * i] = round_decimal(row[6 + 2 * i] + op['tax_values'], self.event.currency)
|
||||
row[9 + 2 * i] = round_decimal(row[9 + 2 * i] + op['prices'], self.event.currency)
|
||||
row[10 + 2 * i] = round_decimal(row[10 + 2 * i] + op['tax_values'], self.event.currency)
|
||||
tax_sums[op['tax_rate']] += op['tax_values']
|
||||
price_sums[op['tax_rate']] += op['prices']
|
||||
|
||||
if row:
|
||||
yield row
|
||||
yield [
|
||||
_('Total'), '', '', '', ''
|
||||
_('Total'), '', '', '', '', '', '', '', ''
|
||||
] + sum(([
|
||||
round_decimal(price_sums.get(t) or Decimal('0.00'), self.event.currency),
|
||||
round_decimal(tax_sums.get(t) or Decimal('0.00'), self.event.currency)
|
||||
|
||||
@@ -213,7 +213,7 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
|
||||
"""
|
||||
|
||||
front_page_top = EventPluginSignal(
|
||||
providing_args=[]
|
||||
providing_args=["request", "subevent"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out to display additional information on the frontpage above the list
|
||||
@@ -236,7 +236,7 @@ receivers are expected to return HTML.
|
||||
"""
|
||||
|
||||
front_page_bottom = EventPluginSignal(
|
||||
providing_args=[]
|
||||
providing_args=["request", "subevent"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out to display additional information on the frontpage below the list
|
||||
@@ -246,6 +246,17 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
|
||||
receivers are expected to return HTML.
|
||||
"""
|
||||
|
||||
front_page_bottom_widget = EventPluginSignal(
|
||||
providing_args=["request", "subevent"]
|
||||
)
|
||||
"""
|
||||
This signal is sent out to display additional information on the frontpage below the list
|
||||
of products if the front page is shown in the widget.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event. The
|
||||
receivers are expected to return HTML.
|
||||
"""
|
||||
|
||||
checkout_all_optional = EventPluginSignal(
|
||||
providing_args=['request']
|
||||
)
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
{% for form in forms %}
|
||||
{% if form.pos.item != pos.item %}
|
||||
{# Add-Ons #}
|
||||
<legend>+ {{ form.pos.item }}</legend>
|
||||
<legend>+ {{ form.pos.item.name }}{% if form.pos.variation %} – {{ form.pos.variation.value }}{% endif %}</legend>
|
||||
{% endif %}
|
||||
{% bootstrap_form form layout="checkout" %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -246,11 +246,31 @@
|
||||
<div class="product">
|
||||
<strong>{% trans "Total" %}</strong>
|
||||
</div>
|
||||
<div class="count hidden-xs">
|
||||
<div class="count hidden-xs hidden-sm">
|
||||
<strong>{{ cart.itemcount }}</strong>
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6 col-md-offset-3 price">
|
||||
<strong>{{ cart.total|money:event.currency }}</strong>
|
||||
|
||||
{% if editable and vouchers_exist and not cart.all_with_voucher %}
|
||||
<br>
|
||||
<a class="js-only apply-voucher-toggle" href="#">
|
||||
<span class="fa fa-tag"></span> {% trans "Redeem a voucher" %}
|
||||
</a>
|
||||
<form action="{% eventurl event "presale:event.cart.voucher" cart_namespace=cart_namespace|default_if_none:"" %}"
|
||||
data-asynctask-headline="{% trans "We're applying this voucher to your cart..." %}"
|
||||
method="post" data-asynctask class="apply-voucher">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="voucher" placeholder="{% trans "Voucher code" %}">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<span class="fa fa-check"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,14 +8,15 @@
|
||||
{% for tup in items_by_category %}
|
||||
<section>
|
||||
{% if tup.0 %}
|
||||
<h3>{{ tup.0.name }}</h3>
|
||||
<h3 id="category-{{ tup.0.id }}">{{ tup.0.name }}</h3>
|
||||
{% if tup.0.description %}
|
||||
<p>{{ tup.0.description|localize|rich_text }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% for item in tup.1 %}
|
||||
{% if item.has_variations %}
|
||||
<details class="item-with-variations" {% if event.settings.show_variations_expanded %}open{% endif %}>
|
||||
<details class="item-with-variations" {% if event.settings.show_variations_expanded %}open{% endif %}
|
||||
id="item-{{ item.id }}">
|
||||
<summary class="row-fluid product-row headline">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{% if item.picture %}
|
||||
@@ -171,7 +172,7 @@
|
||||
</div>
|
||||
</details>
|
||||
{% else %}
|
||||
<div class="row-fluid product-row simple">
|
||||
<div class="row-fluid product-row simple" id="item-{{ item.id }}">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{% if item.picture %}
|
||||
<a href="{{ item.picture.url }}" class="productpicture"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user