Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
90b5d721cb Run pytest with -v 2020-04-22 12:24:56 +02:00
310 changed files with 66926 additions and 89029 deletions

View File

@@ -4,12 +4,10 @@ on:
push:
branches: [ master ]
paths:
- 'doc/**'
- 'src/pretix/locale/**'
pull_request:
branches: [ master ]
paths:
- 'doc/**'
- 'src/pretix/locale/**'
jobs:

View File

@@ -5,12 +5,10 @@ on:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
pull_request:
branches: [ master ]
paths-ignore:
- 'doc/**'
- 'src/pretix/locale/**'
jobs:
test:
@@ -66,7 +64,7 @@ jobs:
run: make all compress
- name: Run tests
working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
run: PRETIX_CONFIG_FILE=tests/travis_${{ matrix.database }}.cfg py.test -v -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 3 tests --maxfail=100
- name: Upload coverage
uses: codecov/codecov-action@v1
with:

View File

@@ -92,11 +92,9 @@ Example::
``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 value is set.
proxy that actively removes and re-adds the header to make sure the correct client IP is the first value.
Defaults to ``off``.
``csp_log``
Log violations of the Content Security Policy (CSP). Defaults to ``on``.
Locale settings
---------------

View File

@@ -30,9 +30,6 @@ position_count integer Number of ticke
checkin_count integer Number of check-ins performed on this list (read-only).
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
@@ -51,11 +48,6 @@ rules object Custom check-in
The ``auto_checkin_sales_channels`` field has been added.
.. versionchanged:: 3.9
The ``subevent`` attribute may now be ``null`` inside event series. The ``allow_multiple_entries``,
``allow_entry_after_exit``, and ``rules`` attributes have been added.
Endpoints
---------
@@ -97,9 +89,6 @@ Endpoints
"limit_products": [],
"include_pending": false,
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"rules": {},
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -144,9 +133,6 @@ Endpoints
"limit_products": [],
"include_pending": false,
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"rules": {},
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -243,8 +229,6 @@ Endpoints
"all_products": false,
"limit_products": [1, 2],
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -267,8 +251,6 @@ Endpoints
"limit_products": [1, 2],
"include_pending": false,
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -321,8 +303,6 @@ Endpoints
"limit_products": [1, 2],
"include_pending": false,
"subevent": null,
"allow_multiple_entries": false,
"allow_entry_after_exit": true,
"auto_checkin_sales_channels": [
"pretixpos"
]
@@ -716,7 +696,6 @@ Order position endpoints
``canceled_supported`` to ``true``, otherwise these orders return ``unpaid``.
* ``already_redeemed`` - Ticket already has been redeemed
* ``product`` - Tickets with this product may not be scanned at this device
* ``rules`` - Check-in prevented by a user-defined rule
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch

View File

@@ -526,7 +526,7 @@ information about the properties.
Get current values of event settings.
Permission required: "Can change event settings" (Exception: with device auth, *some* settings can always be *read*.)
Permission required: "Can change event settings"
**Example request**:

View File

@@ -155,10 +155,6 @@ last_modified datetime Last modificati
The ``reactivate`` operation has been added.
.. versionchanged:: 3.10
The ``search`` query parameter has been added.
.. _order-position-resource:
@@ -199,7 +195,6 @@ pseudonymization_id string A random ID, e.
checkins list of objects List of check-ins with this ticket
├ list integer Internal ID of the check-in list
├ datetime datetime Time of check-in
├ type string Type of scan (defaults to ``entry``)
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
downloads list of objects List of ticket download options
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
@@ -256,10 +251,6 @@ pdf_data object Data object req
The attributes ``company``, ``street``, ``zipcode``, ``city``, ``country``, and ``state`` have been added.
.. versionchanged:: 3.9
The ``checkin.type`` attribute has been added.
.. _order-payment-resource:
Order payment resource
@@ -422,7 +413,6 @@ List of all orders
"checkins": [
{
"list": 44,
"type": "entry",
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
@@ -472,7 +462,6 @@ List of all orders
``last_modified``, and ``status``. Default: ``datetime``
:query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above)
:query string search: Only return orders matching a given search query
: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.
@@ -586,7 +575,6 @@ Fetching individual orders
"checkins": [
{
"list": 44,
"type": "entry",
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
@@ -1483,7 +1471,6 @@ List of all order positions
"checkins": [
{
"list": 44,
"type": "entry",
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}
@@ -1589,7 +1576,6 @@ Fetching individual positions
"checkins": [
{
"list": 44,
"type": "entry",
"datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false
}

View File

@@ -26,8 +26,6 @@ close_when_sold_out boolean If ``true``, th
again.
closed boolean Whether the quota is currently closed (see above
field).
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
have been scanned at an exit.
===================================== ========================== =======================================================
.. versionchanged:: 1.10
@@ -38,10 +36,6 @@ release_after_exit boolean Whether the quo
The attributes ``close_when_sold_out`` and ``closed`` have been added.
.. versionchanged:: 3.10
The attribute ``release_after_exit`` has been added.
Endpoints
---------
@@ -289,7 +283,6 @@ Endpoints
"total_size": 1000,
"pending_orders": 25,
"paid_orders": 423,
"exited_orders": 0,
"cart_positions": 7,
"blocking_vouchers": 126,
"waiting_list": 0

View File

@@ -39,12 +39,10 @@ geo_lon float Longitude of th
item_price_overrides list of objects List of items for which this sub-event overrides the
default price
├ item integer The internal item ID
├ disabled boolean If ``true``, item should not be available for this sub-event
└ price money (string) The price or ``null`` for the default price
variation_price_overrides list of objects List of variations for which this sub-event overrides
the default price
├ variation integer The internal variation ID
├ disabled boolean If ``true``, variation should not be available for this sub-event
└ price money (string) The price or ``null`` for the default price
meta_data object Values set for organizer-specific meta data parameters.
seating_plan integer If reserved seating is in use, the ID of a seating
@@ -76,10 +74,6 @@ seat_category_mapping object An object mappi
The attributes ``geo_lat`` and ``geo_lon`` have been added.
.. versionchanged:: 3.10
The ``disabled`` attribute has been added to ``item_price_overrides`` and ``variation_price_overrides``.
Endpoints
---------
@@ -131,7 +125,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],
@@ -189,7 +182,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],
@@ -224,7 +216,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],
@@ -280,7 +271,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],
@@ -317,7 +307,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "23.42"
}
],
@@ -350,7 +339,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "23.42"
}
],
@@ -439,7 +427,6 @@ Endpoints
"item_price_overrides": [
{
"item": 2,
"disabled": false,
"price": "12.00"
}
],

View File

@@ -70,9 +70,6 @@ and ``checkin_list``.
only include the minimum amount of data necessary for you to fetch the changed objects from our
:ref:`rest-api` in an authenticated way.
.. warning:: In very rare cases, you could receive the same webhook notification twice. We try to avoid it, but we
prefer it over missing a notification.
If you want to further prevent others from accessing your webhook URL, you can also use `Basic authentication`_ and
supply the URL to us in the format of ``https://username:password@domain.com/path/``.
We recommend that you use HTTPS for your webhook URL and might require it in the future. If HTTPS is used, we require

View File

@@ -29,22 +29,6 @@ that we'll provide in this plugin::
from .exporter import MyExporter
return MyExporter
Some exporters might also prove to be useful, when provided on an organizer-level. In order to declare your
exporter as capable of providing exports spanning multiple events, your plugin should listen for this signal
and return the subclass of ``pretix.base.exporter.BaseExporter`` that we'll provide in this plugin::
from django.dispatch import receiver
from pretix.base.signals import register_multievent_data_exporters
@receiver(register_multievent_data_exporters, dispatch_uid="multieventexporter_myexporter")
def register_multievent_data_exporter(sender, **kwargs):
from .exporter import MyExporter
return MyExporter
If your exporter supports both event-level and multi-event level exports, you will need to listen for both
signals.
The exporter class
------------------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,94 +0,0 @@
.. _timeslots:
Use case: Time slots
====================
A more advanced use case of pretix is using pretix for time-slot-based access to an area with a limited visitor
capacity, such as a museum or other attraction. This guide will show you the quickest way to set up such an event
with pretix.
First of all, when creating your event, you need to select that your event represents an "event series":
.. thumbnail:: ../../../screens/event/create_step1.png
:align: center
:class: screenshot
You can click :ref:`here <subevents>` for a more general description of event series with pretix, but everything you
need to know is in this chapter as well.
General event setup
-------------------
Before you go further, set up your products that you want to sell for each time slot, such as different types of entry.
Creating slots
--------------
To create the time slots, you need to create a number of "dates" in the event series. Select "Dates" in the navigation
menu on the left side and click "Create many new dates". Then, first enter the pattern of your opening days. In the
example, the museum is open week Tuesday to Sunday. We recommend to create the slots for a few weeks at a time, but not
e.g. for a full year, since it will be more complicated to change things later.
.. thumbnail:: ../../../screens/event/timeslots_create.png
:align: center
:class: screenshot
Then, scroll to the times section and create your time slots. You can do any interval you like. If you have different
opening times on different week days, you will need to go through the creation process multiple times.
.. thumbnail:: ../../../screens/event/timeslots_create_2.png
:align: center
:class: screenshot
Scroll further down and create one or multiple quotas that define how many people can book a ticket for that time slot.
In this example, 50 people in total are allowed to enter within every slot:
.. thumbnail:: ../../../screens/event/timeslots_create_3.png
:align: center
:class: screenshot
Do **not** create a check-in list at this point. We will deal with this further below in the guide.
Now, press "Save" to create your slots.
.. warning:: If you create a lot of time slots at once, the server might need a few minutes to create them all in our
system. If you receive an error page because it took too long, please do not try again immediately but wait
for a few minutes. Most likely, the slots will be created successfully even though you saw an error.
Event settings
--------------
We recommend that you navigate to "Settings" > "General" > "Display" and set the settings "Default overview style"
to "Week calendar":
.. thumbnail:: ../../../screens/event/timeslots_settings_1.png
:align: center
:class: screenshot
Now, your ticket shop should give users a nice weekly overview over all time slots and their availability:
.. thumbnail:: ../../../screens/event/timeslots_presale.png
:align: center
:class: screenshot
Check-in
--------
If you want to scan people at the entrance of your event and only admit them at their designated time, we recommend
the following setup: Go to "Check-in" in the main navigation on the left and create a new check-in list. Give it a name
and do *not* choose a specific data. We will use one check-in list for all dates. Then, go to the "Advanced" tab at
the top and set up two restrictions to make sure people can only get in during the time slot they registered for.
You can create the rules exactly like shown in the following screenshot:
.. thumbnail:: ../../../screens/event/timeslots_checkinlists.png
:align: center
:class: screenshot
If you want, you can enter a tolerance of e.g. "10" if you want to be a little bit more relaxed and admit people up to
10 minutes before or after their time slot.
Now, download our `Android or Desktop app`_ and register it to your account. The app will ask you to select one the
time slots, but it does not matter, you can select any one of them and then select your newly created check-in list.
That's it, you're good to go!
.. _Android or Desktop app: https://pretix.eu/about/en/scan

View File

@@ -344,13 +344,3 @@ In addition to your normal conference quota, you need to create an unlimited quo
Then, head to the **Bundled products** tab of the "conference ticket" and add the "conference food" as a bundled product with a **designated price** of € 150.
Once a customer tries to buy the € 450 conference ticket, a sub-product will be added and the price will automatically be split into the two components, leading to a correct computation of taxes.
You can find more use cases in these specialized guides:
More use cases
--------------
.. toctree::
:maxdepth: 1
guides/timeslots

View File

@@ -136,15 +136,10 @@ If you want to include all your public events, you can just reference your organ
<pretix-widget event="https://pretix.eu/demo/"></pretix-widget>
There is an optional ``style`` parameter that let's you choose between a monthly calendar view, a week view and a list
view. If you do not set it, the choice will be taken from your organizer settings::
There is an optional ``style`` parameter that let's you choose between a calendar view and a list view. If you do not set it, the choice will be taken from your organizer settings::
<pretix-widget event="https://pretix.eu/demo/series/" style="list"></pretix-widget>
<pretix-widget event="https://pretix.eu/demo/series/" style="calendar"></pretix-widget>
<pretix-widget event="https://pretix.eu/demo/series/" style="week"></pretix-widget>
If you have more than 100 events, the system might refuse to show a list view and always show a calendar for performance
reasons instead.
You can see an example here:

View File

@@ -58,6 +58,28 @@ method without creating a new order. If payment deadlines were dependent on the
forth could either allow someone to extend their deadline forever, or render someones order invalid by moving the date
back in the past.
How can I revert a check-in?
----------------------------
Neither our apps nor our web interface can currently undo the check-in of a tickets. We know that this is
inconvenient for some of you, but we have a good reason for it:
Our Desktop and Android apps both support an asynchronous mode in which they can scan tickets while staying
independent of their internet connection. When scanning with multiple devices, it can of course happen that two
devices scan the same ticket without knowing of the other scan. As soon as one of the devices regains connectivity, it
will upload its activity and the server marks the ticket as checked in -- regardless of the order in which the two
scans were made and uploaded (which could be two different orders).
If we'd provide a "check out" feature, it would not only be used to fix an accidental scan, but scan at entry and
exit to count the current number of people inside etc. In this case, the order of operations matters very much for them
to make sense and provide useful results. This makes implementing an asynchronous mode much more complicated.
In this trade off, we chose offline-capabilities over the check out feature. We plan on solving this problem in the
future, but we're not there yet.
If you're just *testing* the check-in capabilities and want to clean out everything for the real process, you can just
delete and re-create the check-in list.
Why does pretix not support any 1D (linear) bar codes?
------------------------------------------------------

View File

@@ -1,8 +1,8 @@
General settings
================
At "Settings" → "Payment", you can configure every aspect related to the payments you want to accept. The "Deadline"
and "Advanced" tabs of the page show a number of general settings that affect all payment methods:
At "Settings" → "Payment", you can configure every aspect related to the payments you want to accept. The upper part
of the page shows a number of general settings that affect all payment methods:
.. thumbnail:: ../../screens/event/settings_payment.png
:align: center

View File

@@ -1 +1 @@
__version__ = "3.10.0"
__version__ = "3.9.0.dev0"

View File

@@ -14,8 +14,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
class Meta:
model = CheckinList
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
'rules')
'include_pending', 'auto_checkin_sales_channels')
def validate(self, data):
data = super().validate(data)
@@ -29,7 +28,9 @@ class CheckinListSerializer(I18nAwareModelSerializer):
raise ValidationError(_('One or more items do not belong to this event.'))
if event.has_subevents:
if full_data.get('subevent') and event != full_data.get('subevent').event:
if not full_data.get('subevent'):
raise ValidationError(_('Subevent cannot be null for event series.'))
if event != full_data.get('subevent').event:
raise ValidationError(_('The subevent does not belong to this event.'))
else:
if full_data.get('subevent'):

View File

@@ -341,13 +341,13 @@ class CloneEventSerializer(EventSerializer):
class SubEventItemSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEventItem
fields = ('item', 'price', 'disabled')
fields = ('item', 'price')
class SubEventItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEventItemVariation
fields = ('variation', 'price', 'disabled')
fields = ('variation', 'price')
class SubEventSerializer(I18nAwareModelSerializer):
@@ -555,7 +555,6 @@ class EventSettingsSerializer(serializers.Serializer):
'meta_noindex',
'redirect_to_checkout_directly',
'frontpage_subevent_ordering',
'event_list_type',
'frontpage_text',
'attendee_names_asked',
'attendee_names_required',
@@ -611,7 +610,6 @@ class EventSettingsSerializer(serializers.Serializer):
'invoice_introductory_text',
'invoice_additional_text',
'invoice_footer_text',
'invoice_eu_currencies',
'cancel_allow_user',
'cancel_allow_user_until',
'cancel_allow_user_paid',
@@ -630,13 +628,9 @@ class EventSettingsSerializer(serializers.Serializer):
super().__init__(*args, **kwargs)
for fname in self.default_fields:
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
if callable(kwargs):
kwargs = kwargs()
kwargs.setdefault('required', False)
kwargs.setdefault('allow_null', True)
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
if callable(form_kwargs):
form_kwargs = form_kwargs()
if 'serializer_class' not in DEFAULTS[fname]:
raise ValidationError('{} has no serializer class'.format(fname))
f = DEFAULTS[fname]['serializer_class'](
@@ -665,40 +659,3 @@ class EventSettingsSerializer(serializers.Serializer):
settings_dict.update(data)
validate_settings(self.event, settings_dict)
return data
class DeviceEventSettingsSerializer(EventSettingsSerializer):
default_fields = [
'locales',
'locale',
'last_order_modification_date',
'show_quota_left',
'max_items_per_order',
'attendee_names_asked',
'attendee_names_required',
'attendee_emails_asked',
'attendee_emails_required',
'attendee_addresses_asked',
'attendee_addresses_required',
'attendee_company_asked',
'attendee_company_required',
'ticket_download',
'ticket_download_addons',
'ticket_download_nonadm',
'ticket_download_pending',
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',
'invoice_address_company_required',
'invoice_address_beneficiary',
'invoice_address_custom_field',
'invoice_name_required',
'invoice_address_not_asked_free',
'invoice_address_from_name',
'invoice_address_from',
'invoice_address_from_zipcode',
'invoice_address_from_city',
'invoice_address_from_country',
'invoice_address_from_tax_id',
'invoice_address_from_vat_id',
]

View File

@@ -349,7 +349,7 @@ class QuotaSerializer(I18nAwareModelSerializer):
class Meta:
model = Quota
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out', 'release_after_exit')
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out')
def validate(self, data):
data = super().validate(data)

View File

@@ -68,7 +68,7 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
data['name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
if data.get('country'):
if not pycountry.countries.get(alpha_2=data.get('country').code):
if not pycountry.countries.get(alpha_2=data.get('country')):
raise ValidationError(
{'country': ['Invalid country code.']}
)
@@ -122,7 +122,7 @@ class AnswerSerializer(I18nAwareModelSerializer):
class CheckinSerializer(I18nAwareModelSerializer):
class Meta:
model = Checkin
fields = ('datetime', 'list', 'auto_checked_in', 'type')
fields = ('datetime', 'list', 'auto_checked_in')
class OrderDownloadsField(serializers.Field):
@@ -416,26 +416,16 @@ class OrderSerializer(I18nAwareModelSerializer):
return instance
class SimulatedOrderPositionSerializer(OrderPositionSerializer):
addon_to = serializers.SlugRelatedField(read_only=True, slug_field='positionid')
class SimulatedOrderSerializer(OrderSerializer):
positions = SimulatedOrderPositionSerializer(many=True, read_only=True)
class PriceCalcSerializer(serializers.Serializer):
item = serializers.PrimaryKeyRelatedField(queryset=Item.objects.none(), required=False, allow_null=True)
variation = serializers.PrimaryKeyRelatedField(queryset=ItemVariation.objects.none(), required=False, allow_null=True)
subevent = serializers.PrimaryKeyRelatedField(queryset=SubEvent.objects.none(), required=False, allow_null=True)
tax_rule = serializers.PrimaryKeyRelatedField(queryset=TaxRule.objects.none(), required=False, allow_null=True)
locale = serializers.CharField(allow_null=True, required=False)
def __init__(self, *args, **kwargs):
event = kwargs.pop('event')
super().__init__(*args, **kwargs)
self.fields['item'].queryset = event.items.all()
self.fields['tax_rule'].queryset = event.tax_rules.all()
self.fields['variation'].queryset = ItemVariation.objects.filter(item__event=event)
if event.has_subevents:
self.fields['subevent'].queryset = event.subevents.all()
@@ -600,7 +590,7 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
if data.get('country'):
if not pycountry.countries.get(alpha_2=data.get('country').code):
if not pycountry.countries.get(alpha_2=data.get('country')):
raise ValidationError(
{'country': ['Invalid country code.']}
)
@@ -913,19 +903,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota:
continue
if pos_data.get('subevent'):
if pos_data.get('item').pk in pos_data['subevent'].item_overrides and pos_data['subevent'].item_overrides[pos_data['item'].pk].disabled:
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
str(pos_data.get('item'))
)]
if (
pos_data.get('variation') and pos_data['variation'].pk in pos_data['subevent'].var_overrides and
pos_data['subevent'].var_overrides[pos_data['variation'].pk].disabled
):
errs[i]['item'] = [gettext_lazy('The product "{}" is not available on this date.').format(
str(pos_data.get('item'))
)]
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
@@ -986,10 +963,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
else:
pos.order = order
if addon_to:
if simulate:
pos.addon_to = pos_map[addon_to]._wrapped
else:
pos.addon_to = pos_map[addon_to]
pos.addon_to = pos_map[addon_to]
if pos.price is None:
price = get_price(

View File

@@ -41,8 +41,8 @@ class ConditionalListView:
return super().list(request, **kwargs)
lmd = request.event.logentry_set.filter(
content_type__model=self.get_queryset().model._meta.model_name,
content_type__app_label=self.get_queryset().model._meta.app_label,
content_type__model=self.queryset.model._meta.model_name,
content_type__app_label=self.queryset.model._meta.app_label,
).aggregate(
m=Max('datetime')
)['m']

View File

@@ -88,9 +88,8 @@ class CheckinListViewSet(viewsets.ModelViewSet):
pqs = OrderPosition.objects.filter(
order__event=clist.event,
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []),
subevent=clist.subevent,
)
if clist.subevent:
pqs = pqs.filter(subevent=clist.subevent)
if not clist.all_products:
pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True))
cqs = cqs.filter(position__item__in=clist.limit_products.values_list('id', flat=True))
@@ -202,13 +201,10 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
qs = OrderPosition.objects.filter(
order__event=self.request.event,
subevent=self.checkinlist.subevent
).annotate(
last_checked_in=Subquery(cqs)
)
if self.checkinlist.subevent:
qs = qs.filter(
subevent=self.checkinlist.subevent
)
if self.request.query_params.get('ignore_status', 'false') != 'true' and not ignore_status:
qs = qs.filter(
@@ -255,9 +251,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
@action(detail=True, methods=['POST'])
def redeem(self, *args, **kwargs):
force = bool(self.request.data.get('force', False))
type = self.request.data.get('type', None) or Checkin.TYPE_ENTRY
if type not in dict(Checkin.CHECKIN_TYPES):
raise ValidationError("Invalid check-in type.")
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
nonce = self.request.data.get('nonce')
op = self.get_object(ignore_status=True)
@@ -290,7 +283,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
canceled_supported=self.request.data.get('canceled_supported', False),
user=self.request.user,
auth=self.request.auth,
type=type,
)
except RequiredQuestionsError as e:
return Response({

View File

@@ -10,8 +10,8 @@ from rest_framework.response import Response
from pretix.api.auth.permission import EventCRUDPermission
from pretix.api.serializers.event import (
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
EventSettingsSerializer, SubEventSerializer, TaxRuleSerializer,
CloneEventSerializer, EventSerializer, EventSettingsSerializer,
SubEventSerializer, TaxRuleSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
@@ -337,16 +337,10 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
class EventSettingsView(views.APIView):
permission = None
write_permission = 'can_change_event_settings'
permission = 'can_change_event_settings'
def get(self, request, *args, **kwargs):
if isinstance(request.auth, Device):
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event)
elif 'can_change_event_settings' in request.eventpermset:
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
else:
raise PermissionDenied()
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
if 'explain' in request.GET:
return Response({
fname: {

View File

@@ -20,7 +20,6 @@ from pretix.base.models import (
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation,
Question, QuestionOption, Quota,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.helpers.dicts import merge_dicts
with scopes_disabled():
@@ -534,18 +533,14 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
def availability(self, request, *args, **kwargs):
quota = self.get_object()
qa = QuotaAvailability()
qa.queue(quota)
qa.compute()
avail = qa.results[quota]
avail = quota.availability()
data = {
'paid_orders': qa.count_paid_orders[quota],
'pending_orders': qa.count_pending_orders[quota],
'exited_orders': qa.count_exited_orders[quota],
'blocking_vouchers': qa.count_vouchers[quota],
'cart_positions': qa.count_cart[quota],
'waiting_list': qa.count_pending_orders[quota],
'paid_orders': quota.count_paid_orders(),
'pending_orders': quota.count_pending_orders(),
'blocking_vouchers': quota.count_blocking_vouchers(),
'cart_positions': quota.count_in_cart(),
'waiting_list': quota.count_waiting_list_pending(),
'available_number': avail[1],
'available': avail[0] == Quota.AVAILABILITY_OK,
'total_size': quota.size,

View File

@@ -4,7 +4,7 @@ from decimal import Decimal
import django_filters
import pytz
from django.db import transaction
from django.db.models import Exists, F, OuterRef, Prefetch, Q
from django.db.models import F, Prefetch, Q
from django.db.models.functions import Coalesce, Concat
from django.http import FileResponse, HttpResponse
from django.shortcuts import get_object_or_404
@@ -26,7 +26,7 @@ from pretix.api.serializers.order import (
InvoiceSerializer, OrderCreateSerializer, OrderPaymentCreateSerializer,
OrderPaymentSerializer, OrderPositionSerializer,
OrderRefundCreateSerializer, OrderRefundSerializer, OrderSerializer,
PriceCalcSerializer, SimulatedOrderSerializer,
PriceCalcSerializer,
)
from pretix.base.i18n import language
from pretix.base.models import (
@@ -52,7 +52,6 @@ from pretix.base.signals import (
order_modified, order_paid, order_placed, register_ticket_outputs,
)
from pretix.base.templatetags.money import money_filter
from pretix.control.signals import order_search_filter_q
with scopes_disabled():
class OrderFilter(FilterSet):
@@ -61,48 +60,11 @@ with scopes_disabled():
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
search = django_filters.CharFilter(method='search_qs')
class Meta:
model = Order
fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval']
def search_qs(self, qs, name, value):
u = value
if "-" in value:
code = (Q(event__slug__icontains=u.rsplit("-", 1)[0])
& Q(code__icontains=Order.normalize_code(u.rsplit("-", 1)[1])))
else:
code = Q(code__icontains=Order.normalize_code(u))
matching_invoices = Invoice.objects.filter(
Q(invoice_no__iexact=u)
| Q(invoice_no__iexact=u.zfill(5))
| Q(full_invoice_no__iexact=u)
).values_list('order_id', flat=True)
matching_positions = OrderPosition.objects.filter(
Q(order=OuterRef('pk')) & Q(
Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u)
| Q(secret__istartswith=u) | Q(voucher__code__icontains=u)
)
).values('id')
mainq = (
code
| Q(email__icontains=u)
| Q(invoice_address__name_cached__icontains=u)
| Q(invoice_address__company__icontains=u)
| Q(pk__in=matching_invoices)
| Q(comment__icontains=u)
| Q(has_pos=True)
)
for recv, q in order_search_filter_q.send(sender=getattr(self, 'event', None), query=u):
mainq = mainq | q
return qs.annotate(has_pos=Exists(matching_positions)).filter(
mainq
)
class OrderViewSet(viewsets.ModelViewSet):
serializer_class = OrderSerializer
@@ -526,12 +488,10 @@ class OrderViewSet(viewsets.ModelViewSet):
self.perform_create(serializer)
send_mail = serializer._send_mail
order = serializer.instance
serializer = OrderSerializer(order, context=serializer.context)
if not order.pk:
# Simulation
serializer = SimulatedOrderSerializer(order, context=serializer.context)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
serializer = OrderSerializer(order, context=serializer.context)
order.log_action(
'pretix.event.order.placed',
@@ -783,8 +743,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
{
"item": 2,
"variation": null,
"subevent": 3,
"tax_rule": 4,
"subevent": 3
}
Sample output:
@@ -838,11 +797,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
if data.get('subevent'):
kwargs['subevent'] = data.get('subevent')
if data.get('tax_rule'):
kwargs['tax_rule'] = data.get('tax_rule')
price = get_price(**kwargs)
tr = kwargs.get('tax_rule', kwargs.get('item').tax_rule)
with language(data.get('locale') or self.request.event.settings.locale):
return Response({
'gross': price.gross,
@@ -851,7 +806,6 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
'rate': price.rate,
'name': str(price.name),
'tax': price.tax,
'tax_rule': tr.pk if tr else None,
})
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')

View File

@@ -168,9 +168,9 @@ def register_default_webhook_events(sender, **kwargs):
)
@app.task(base=TransactionAwareTask, acks_late=True)
@app.task(base=TransactionAwareTask)
def notify_webhooks(logentry_id: int):
logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id)
logentry = LogEntry.all.get(id=logentry_id)
if not logentry.organizer:
return # We need to know the organizer
@@ -205,7 +205,7 @@ def notify_webhooks(logentry_id: int):
send_webhook.apply_async(args=(logentry_id, notification_type.action_type, wh.pk))
@app.task(base=ProfiledTask, bind=True, max_retries=9, acks_late=True)
@app.task(base=ProfiledTask, bind=True, max_retries=9)
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
# 9 retries with 2**(2*x) timing is roughly 72 hours
with scopes_disabled():

View File

@@ -6,14 +6,11 @@ from typing import Tuple
from defusedcsv import csv
from django import forms
from django.db.models import QuerySet
from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import KNOWN_TYPES
from pretix.base.models import Event
class BaseExporter:
"""
@@ -22,12 +19,6 @@ class BaseExporter:
def __init__(self, event):
self.event = event
self.is_multievent = isinstance(event, QuerySet)
if isinstance(event, QuerySet):
self.events = event
self.event = None
else:
self.events = Event.objects.filter(pk=event.pk)
def __str__(self):
return self.identifier

View File

@@ -9,14 +9,11 @@ from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from pretix.base.models import Invoice, OrderPayment
from pretix.base.models import OrderPayment
from ...control.forms.filter import get_all_payment_providers
from ..exporter import BaseExporter
from ..services.invoices import invoice_pdf_task
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
from ..signals import register_data_exporters
class InvoiceExporter(BaseExporter):
@@ -24,7 +21,7 @@ class InvoiceExporter(BaseExporter):
verbose_name = _('All invoices')
def render(self, form_data: dict, output_file=None):
qs = Invoice.objects.filter(event__in=self.events, shredded=False)
qs = self.event.invoices.filter(shredded=False)
if form_data.get('payment_provider'):
qs = qs.annotate(
@@ -71,16 +68,11 @@ class InvoiceExporter(BaseExporter):
if not any:
return None
if self.is_multievent:
filename = '{}_invoices.zip'.format(self.events.first().organizer.slug)
else:
filename = '{}_invoices.zip'.format(self.event.slug)
if output_file:
return filename, 'application/zip', None
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', None
else:
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return filename, 'application/zip', zipf.read()
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
@property
def export_form_fields(self):
@@ -107,8 +99,6 @@ class InvoiceExporter(BaseExporter):
label=_('Payment provider'),
choices=[
('', _('All payment providers')),
] + get_all_payment_providers() if self.is_multievent else [
('', _('All payment providers')),
] + [
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
],
@@ -125,8 +115,3 @@ class InvoiceExporter(BaseExporter):
@receiver(register_data_exporters, dispatch_uid="exporter_invoices")
def register_invoice_export(sender, **kwargs):
return InvoiceExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_invoices")
def register_multievent_invoice_export(sender, **kwargs):
return InvoiceExporter

View File

@@ -8,9 +8,7 @@ from pretix.base.models import OrderPosition
from ..exporter import BaseExporter
from ..models import Order
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
from ..signals import register_data_exporters
class MailExporter(BaseExporter):
@@ -18,18 +16,14 @@ class MailExporter(BaseExporter):
verbose_name = _('Email addresses (text file)')
def render(self, form_data: dict):
qs = Order.objects.filter(event__in=self.events, status__in=form_data['status']).prefetch_related('event')
qs = self.event.orders.filter(status__in=form_data['status'])
addrs = qs.values('email')
pos = OrderPosition.objects.filter(
order__event__in=self.events, order__status__in=form_data['status']
order__event=self.event, order__status__in=form_data['status']
).values('attendee_email')
data = "\r\n".join(set(a['email'] for a in addrs if a['email'])
| set(a['attendee_email'] for a in pos if a['attendee_email']))
if self.is_multievent:
return '{}_pretixemails.txt'.format(self.events.first().organizer.slug), 'text/plain', data.encode("utf-8")
else:
return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8")
return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8")
@property
def export_form_fields(self):
@@ -50,8 +44,3 @@ class MailExporter(BaseExporter):
@receiver(register_data_exporters, dispatch_uid="exporter_mail")
def register_mail_export(sender, **kwargs):
return MailExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_mail")
def register_multievent_mail_export(sender, **kwargs):
return MailExporter

View File

@@ -4,38 +4,26 @@ from decimal import Decimal
import pytz
from django import forms
from django.db.models import (
CharField, Count, DateTimeField, F, IntegerField, Max, OuterRef, Subquery,
Sum,
Count, DateTimeField, F, IntegerField, Max, OuterRef, Subquery, Sum,
)
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.translation import gettext as _, gettext_lazy, pgettext
from pretix.base.models import (
GiftCard, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
Question,
GiftCard, InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.settings import PERSON_NAME_SCHEMES
from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ..exporter import ListExporter, MultiSheetListExporter
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
from ..signals import register_data_exporters
class OrderListExporter(MultiSheetListExporter):
identifier = 'orderlist'
verbose_name = gettext_lazy('Order data')
@cached_property
def providers(self):
return dict(get_all_payment_providers())
@property
def sheets(self):
return (
@@ -61,13 +49,13 @@ class OrderListExporter(MultiSheetListExporter):
tax_rates = set(
a for a
in OrderFee.objects.filter(
order__event__in=self.events
order__event=self.event
).values_list('tax_rate', flat=True).distinct().order_by()
)
tax_rates |= set(
a for a
in OrderPosition.objects.filter(
order__event__in=self.events
order__event=self.event
).values_list('tax_rate', flat=True).distinct().order_by()
)
tax_rates = sorted(tax_rates)
@@ -82,6 +70,8 @@ class OrderListExporter(MultiSheetListExporter):
return self.iterate_fees(form_data)
def iterate_orders(self, form_data: dict):
tz = pytz.timezone(self.event.settings.timezone)
p_date = OrderPayment.objects.filter(
order=OuterRef('pk'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
@@ -91,34 +81,24 @@ class OrderListExporter(MultiSheetListExporter):
).values(
'm'
).order_by()
p_providers = OrderPayment.objects.filter(
order=OuterRef('pk'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
).values('order').annotate(
m=GroupConcat('provider', delimiter=',')
).values(
'm'
).order_by()
s = OrderPosition.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(k=Count('id')).values('k')
qs = Order.objects.filter(event__in=self.events).annotate(
qs = self.event.orders.annotate(
payment_date=Subquery(p_date, output_field=DateTimeField()),
payment_providers=Subquery(p_providers, output_field=CharField()),
pcnt=Subquery(s, output_field=IntegerField())
).select_related('invoice_address').prefetch_related('invoices').prefetch_related('event')
).select_related('invoice_address').prefetch_related('invoices')
if form_data['paid_only']:
qs = qs.filter(status=Order.STATUS_PAID)
tax_rates = self._get_all_tax_rates(qs)
headers = [
_('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Company'), _('Name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
if name_scheme and len(name_scheme['fields']) > 1:
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(label)
headers += [
@@ -138,7 +118,6 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Requires special attention'))
headers.append(_('Comment'))
headers.append(_('Positions'))
headers.append(_('Payment providers'))
yield headers
@@ -160,10 +139,7 @@ class OrderListExporter(MultiSheetListExporter):
}
for order in qs.order_by('datetime'):
tz = pytz.timezone(order.event.settings.timezone)
row = [
order.event.slug,
order.code,
order.total,
order.get_status_display(),
@@ -175,7 +151,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.company,
order.invoice_address.name,
]
if name_scheme and len(name_scheme['fields']) > 1:
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
@@ -190,7 +166,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
@@ -217,32 +193,18 @@ class OrderListExporter(MultiSheetListExporter):
row.append(_('Yes') if order.checkin_attention else _('No'))
row.append(order.comment or "")
row.append(order.pcnt)
row.append(', '.join([
str(self.providers.get(p, p)) for p in sorted(set((order.payment_providers or '').split(',')))
if p and p != 'free'
]))
yield row
def iterate_fees(self, form_data: dict):
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
).values('order').annotate(
m=GroupConcat('provider', delimiter=',')
).values(
'm'
).order_by()
tz = pytz.timezone(self.event.settings.timezone)
qs = OrderFee.objects.filter(
order__event__in=self.events,
).annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
order__event=self.event,
).select_related('order', 'order__invoice_address', 'tax_rule')
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID)
headers = [
_('Event slug'),
_('Order code'),
_('Status'),
_('Email'),
@@ -256,22 +218,19 @@ class OrderListExporter(MultiSheetListExporter):
_('Company'),
_('Invoice address name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
if name_scheme and len(name_scheme['fields']) > 1:
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
]
headers.append(_('Payment providers'))
yield headers
for op in qs.order_by('order__datetime'):
order = op.order
tz = pytz.timezone(order.event.settings.timezone)
row = [
order.event.slug,
order.code,
order.get_status_display(),
order.email,
@@ -288,7 +247,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.company,
order.invoice_address.name,
]
if name_scheme and len(name_scheme['fields']) > 1:
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
@@ -303,27 +262,14 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0))
row.append(', '.join([
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
if p and p != 'free'
]))
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
yield row
def iterate_positions(self, form_data: dict):
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
).values('order').annotate(
m=GroupConcat('provider', delimiter=',')
).values(
'm'
).order_by()
tz = pytz.timezone(self.event.settings.timezone)
qs = OrderPosition.objects.filter(
order__event__in=self.events,
).annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
order__event=self.event,
).select_related(
'order', 'order__invoice_address', 'item', 'variation',
'voucher', 'tax_rule'
@@ -334,14 +280,13 @@ class OrderListExporter(MultiSheetListExporter):
qs = qs.filter(order__status=Order.STATUS_PAID)
headers = [
_('Event slug'),
_('Order code'),
_('Position ID'),
_('Status'),
_('Email'),
_('Order date'),
]
if self.events.filter(has_subevents=True).exists():
if self.event.has_subevents:
headers.append(pgettext('subevent', 'Date'))
headers.append(_('Start date'))
headers.append(_('End date'))
@@ -354,8 +299,8 @@ class OrderListExporter(MultiSheetListExporter):
_('Tax value'),
_('Attendee name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
if name_scheme and len(name_scheme['fields']) > 1:
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Attendee name') + ': ' + str(label))
headers += [
@@ -369,8 +314,7 @@ class OrderListExporter(MultiSheetListExporter):
_('Voucher'),
_('Pseudonymization ID'),
]
questions = list(Question.objects.filter(event__in=self.events))
questions = list(self.event.questions.all())
options = {}
for q in questions:
if q.type == Question.TYPE_CHOICE_MULTIPLE:
@@ -384,35 +328,30 @@ class OrderListExporter(MultiSheetListExporter):
_('Company'),
_('Invoice address name'),
]
if name_scheme and len(name_scheme['fields']) > 1:
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
]
headers += [
_('Sales channel'), _('Order locale'),
_('Payment providers'),
]
headers.append(_('Sales channel'))
yield headers
for op in qs.order_by('order__datetime', 'positionid'):
order = op.order
tz = pytz.timezone(order.event.settings.timezone)
row = [
order.event.slug,
order.code,
op.positionid,
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
]
if order.event.has_subevents:
if self.event.has_subevents:
row.append(op.subevent.name)
row.append(op.subevent.date_from.astimezone(order.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
row.append(op.subevent.date_from.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
if op.subevent.date_to:
row.append(op.subevent.date_to.astimezone(order.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
row.append(op.subevent.date_to.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
else:
row.append('')
row += [
@@ -424,7 +363,7 @@ class OrderListExporter(MultiSheetListExporter):
op.tax_value,
op.attendee_name,
]
if name_scheme and len(name_scheme['fields']) > 1:
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
op.attendee_name_parts.get(k, '')
@@ -462,7 +401,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.company,
order.invoice_address.name,
]
if name_scheme and len(name_scheme['fields']) > 1:
if len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
@@ -477,22 +416,12 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0))
row += [
order.sales_channel,
order.locale
]
row.append(', '.join([
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
if p and p != 'free'
]))
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row.append(order.sales_channel)
yield row
def get_filename(self):
if self.is_multievent:
return '{}_orders'.format(self.events.first().organizer.slug)
else:
return '{}_orders'.format(self.event.slug)
return '{}_orders'.format(self.event.slug)
class PaymentListExporter(ListExporter):
@@ -503,48 +432,47 @@ class PaymentListExporter(ListExporter):
def additional_form_fields(self):
return OrderedDict(
[
('payment_states',
forms.MultipleChoiceField(
label=_('Payment states'),
choices=OrderPayment.PAYMENT_STATES,
initial=[OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED],
required=False,
widget=forms.CheckboxSelectMultiple,
)),
('refund_states',
forms.MultipleChoiceField(
label=_('Refund states'),
choices=OrderRefund.REFUND_STATES,
initial=[OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_CREATED,
OrderRefund.REFUND_STATE_TRANSIT],
widget=forms.CheckboxSelectMultiple,
('successful_only',
forms.BooleanField(
label=_('Only successful payments'),
initial=True,
required=False
)),
]
)
def iterate_list(self, form_data):
provider_names = dict(get_all_payment_providers())
tz = pytz.timezone(self.event.settings.timezone)
provider_names = {
k: v.verbose_name
for k, v in self.event.get_payment_providers().items()
}
payments = OrderPayment.objects.filter(
order__event__in=self.events,
state__in=form_data.get('payment_states', [])
order__event=self.event,
).order_by('created')
refunds = OrderRefund.objects.filter(
order__event__in=self.events,
state__in=form_data.get('refund_states', [])
order__event=self.event
).order_by('created')
if form_data['successful_only']:
payments = payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
)
refunds = refunds.filter(
state=OrderRefund.REFUND_STATE_DONE,
)
objs = sorted(list(payments) + list(refunds), key=lambda o: o.created)
headers = [
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Status code'), _('Amount'), _('Payment method')
]
yield headers
for obj in objs:
tz = pytz.timezone(obj.order.event.settings.timezone)
if isinstance(obj, OrderPayment) and obj.payment_date:
d2 = obj.payment_date.astimezone(tz).date().strftime('%Y-%m-%d')
elif isinstance(obj, OrderRefund) and obj.execution_date:
@@ -552,7 +480,6 @@ class PaymentListExporter(ListExporter):
else:
d2 = ''
row = [
obj.order.event.slug,
obj.order.code,
obj.full_id,
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
@@ -565,10 +492,7 @@ class PaymentListExporter(ListExporter):
yield row
def get_filename(self):
if self.is_multievent:
return '{}_payments'.format(self.events.first().organizer.slug)
else:
return '{}_payments'.format(self.event.slug)
return '{}_payments'.format(self.event.slug)
class QuotaListExporter(ListExporter):
@@ -578,26 +502,20 @@ class QuotaListExporter(ListExporter):
def iterate_list(self, form_data):
headers = [
_('Quota name'), _('Total quota'), _('Paid orders'), _('Pending orders'), _('Blocking vouchers'),
_('Current user\'s carts'), _('Waiting list'), _('Exited orders'), _('Current availability')
_('Current user\'s carts'), _('Waiting list'), _('Current availability')
]
yield headers
quotas = list(self.event.quotas.all())
qa = QuotaAvailability(full_results=True)
qa.queue(*quotas)
qa.compute()
for quota in quotas:
avail = qa.results[quota]
for quota in self.event.quotas.all():
avail = quota.availability()
row = [
quota.name,
_('Infinite') if quota.size is None else quota.size,
qa.count_paid_orders[quota],
qa.count_pending_orders[quota],
qa.count_vouchers[quota],
qa.count_cart[quota],
qa.count_waitinglist[quota],
qa.count_exited_orders[quota],
quota.count_paid_orders(),
quota.count_pending_orders(),
quota.count_blocking_vouchers(),
quota.count_in_cart(),
quota.count_waiting_list_pending(),
_('Infinite') if avail[1] is None else avail[1]
]
yield row
@@ -650,21 +568,10 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Total value (with taxes)'),
_('Total value (without taxes)'),
_('Payment matching IDs'),
_('Payment providers'),
]
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
).values('order').annotate(
m=GroupConcat('provider', delimiter=',')
).values(
'm'
).order_by()
qs = Invoice.objects.filter(event__in=self.events).order_by('full_invoice_no').select_related(
qs = self.event.invoices.order_by('full_invoice_no').select_related(
'order', 'refers'
).prefetch_related('order__payments').annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
total_gross=Subquery(
InvoiceLine.objects.filter(
invoice=OuterRef('pk')
@@ -721,11 +628,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
i.foreign_currency_rate,
i.total_gross if i.total_gross else Decimal('0.00'),
Decimal(i.total_net if i.total_net else '0.00').quantize(Decimal('0.01')),
pmi,
', '.join([
str(self.providers.get(p, p)) for p in sorted(set((i.payment_providers or '').split(',')))
if p and p != 'free'
])
pmi
]
elif sheet == 'lines':
yield [
@@ -761,21 +664,9 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Invoice recipient:') + ' ' + _('VAT ID'),
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
_('Payment providers'),
]
p_providers = OrderPayment.objects.filter(
order=OuterRef('invoice__order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),
).values('order').annotate(
m=GroupConcat('provider', delimiter=',')
).values(
'm'
).order_by()
qs = InvoiceLine.objects.annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
).filter(
invoice__event__in=self.events
qs = InvoiceLine.objects.filter(
invoice__event=self.event
).order_by('invoice__full_invoice_no', 'position').select_related(
'invoice', 'invoice__order', 'invoice__refers'
)
@@ -813,21 +704,10 @@ class InvoiceDataExporter(MultiSheetListExporter):
i.invoice_to_vat_id,
i.invoice_to_beneficiary,
i.internal_reference,
', '.join([
str(self.providers.get(p, p)) for p in sorted(set((l.payment_providers or '').split(',')))
if p and p != 'free'
])
]
@cached_property
def providers(self):
return dict(get_all_payment_providers())
def get_filename(self):
if self.is_multievent:
return '{}_invoices'.format(self.events.first().organizer.slug)
else:
return '{}_invoices'.format(self.event.slug)
return '{}_invoices'.format(self.event.slug)
class GiftcardRedemptionListExporter(ListExporter):
@@ -835,27 +715,27 @@ class GiftcardRedemptionListExporter(ListExporter):
verbose_name = gettext_lazy('Gift card redemptions')
def iterate_list(self, form_data):
tz = pytz.timezone(self.event.settings.timezone)
payments = OrderPayment.objects.filter(
order__event__in=self.events,
order__event=self.event,
provider='giftcard'
).order_by('created')
refunds = OrderRefund.objects.filter(
order__event__in=self.events,
order__event=self.event,
provider='giftcard'
).order_by('created')
objs = sorted(list(payments) + list(refunds), key=lambda o: (o.order.code, o.created))
headers = [
_('Event slug'), _('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer')
_('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer')
]
yield headers
for obj in objs:
tz = pytz.timezone(obj.order.event.settings.timezone)
gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card'))
row = [
obj.order.event.slug,
obj.order.code,
obj.full_id,
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
@@ -866,10 +746,7 @@ class GiftcardRedemptionListExporter(ListExporter):
yield row
def get_filename(self):
if self.is_multievent:
return '{}_giftcardredemptions'.format(self.events.first().organizer.slug)
else:
return '{}_giftcardredemptions'.format(self.event.slug)
return '{}_giftcardredemptions'.format(self.event.slug)
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
@@ -877,21 +754,11 @@ def register_orderlist_exporter(sender, **kwargs):
return OrderListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_orderlist")
def register_multievent_orderlist_exporter(sender, **kwargs):
return OrderListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_paymentlist")
def register_paymentlist_exporter(sender, **kwargs):
return PaymentListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_paymentlist")
def register_multievent_paymentlist_exporter(sender, **kwargs):
return PaymentListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_quotalist")
def register_quotalist_exporter(sender, **kwargs):
return QuotaListExporter
@@ -902,16 +769,6 @@ def register_invoicedata_exporter(sender, **kwargs):
return InvoiceDataExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_invoicedata")
def register_multievent_invoicedatae_xporter(sender, **kwargs):
return InvoiceDataExporter
@receiver(register_data_exporters, dispatch_uid="exporter_giftcardredemptionlist")
def register_giftcardredemptionlist_exporter(sender, **kwargs):
return GiftcardRedemptionListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_giftcardredemptionlist")
def register_multievent_i_giftcardredemptionlist_exporter(sender, **kwargs):
return GiftcardRedemptionListExporter

View File

@@ -48,9 +48,6 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet):
super().__init__(*args, **kwargs)
SECRET_REDACTED = '*****'
class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
auto_fields = []
@@ -65,8 +62,6 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
super().__init__(*args, **kwargs)
for fname in self.auto_fields:
kwargs = DEFAULTS[fname].get('form_kwargs', {})
if callable(kwargs):
kwargs = kwargs()
kwargs.setdefault('required', False)
field = DEFAULTS[fname]['form_class'](
**kwargs
@@ -78,12 +73,6 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
f.set_event(self.obj)
def save(self):
for k, v in self.cleaned_data.items():
if isinstance(self.fields.get(k), SecretKeySettingsField) and self.cleaned_data.get(k) == SECRET_REDACTED:
self.cleaned_data[k] = self.initial[k]
return super().save()
def get_new_filename(self, name: str) -> str:
from pretix.base.models import Event
@@ -122,32 +111,3 @@ class SafeSessionWizardView(SessionWizardView):
}
)
return context
class SecretKeySettingsWidget(forms.TextInput):
def __init__(self, attrs=None):
if attrs is None:
attrs = {}
attrs.update({
'autocomplete': 'new-password' # see https://bugs.chromium.org/p/chromium/issues/detail?id=370363#c7
})
super().__init__(attrs)
def get_context(self, name, value, attrs):
if value:
value = SECRET_REDACTED
return super().get_context(name, value, attrs)
class SecretKeySettingsField(forms.CharField):
widget = SecretKeySettingsWidget
def has_changed(self, initial, data):
if data == SECRET_REDACTED:
return False
return super().has_changed(initial, data)
def run_validators(self, value):
if value == SECRET_REDACTED:
return
return super().run_validators(value)

View File

@@ -40,7 +40,7 @@ from pretix.base.settings import (
PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.control.forms import ExtFileField, SplitDateTimeField
from pretix.control.forms import SplitDateTimeField
from pretix.helpers.countries import CachedCountries
from pretix.helpers.escapejson import escapejson_attr
from pretix.helpers.i18n import get_format_without_seconds
@@ -205,13 +205,12 @@ def guess_country(event):
valid_countries = countries.countries
if '-' in locale:
parts = locale.split('-')
# TODO: does this actually work?
if parts[1].upper() in valid_countries:
country = Country(parts[1].upper())
elif parts[0].upper() in valid_countries:
country = Country(parts[0].upper())
else:
if locale.upper() in valid_countries:
if locale in valid_countries:
country = Country(locale.upper())
return country
@@ -244,10 +243,8 @@ class BaseQuestionsForm(forms.Form):
super().__init__(*args, **kwargs)
add_fields = {}
if item.admission and event.settings.attendee_names_asked:
add_fields['attendee_name_parts'] = NamePartsFormField(
self.fields['attendee_name_parts'] = NamePartsFormField(
max_length=255,
required=event.settings.attendee_names_required and not self.all_optional,
scheme=event.settings.name_scheme,
@@ -256,7 +253,7 @@ class BaseQuestionsForm(forms.Form):
initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
)
if item.admission and event.settings.attendee_emails_asked:
add_fields['attendee_email'] = forms.EmailField(
self.fields['attendee_email'] = forms.EmailField(
required=event.settings.attendee_emails_required and not self.all_optional,
label=_('Attendee email'),
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
@@ -267,15 +264,14 @@ class BaseQuestionsForm(forms.Form):
)
)
if item.admission and event.settings.attendee_company_asked:
add_fields['company'] = forms.CharField(
self.fields['company'] = forms.CharField(
required=event.settings.attendee_company_required and not self.all_optional,
label=_('Company'),
max_length=255,
initial=(cartpos.company if cartpos else orderpos.company),
)
if item.admission and event.settings.attendee_addresses_asked:
add_fields['street'] = forms.CharField(
self.fields['street'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('Address'),
widget=forms.Textarea(attrs={
@@ -285,26 +281,24 @@ class BaseQuestionsForm(forms.Form):
}),
initial=(cartpos.street if cartpos else orderpos.street),
)
add_fields['zipcode'] = forms.CharField(
self.fields['zipcode'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
max_length=30,
label=_('ZIP code'),
initial=(cartpos.zipcode if cartpos else orderpos.zipcode),
widget=forms.TextInput(attrs={
'autocomplete': 'postal-code',
}),
)
add_fields['city'] = forms.CharField(
self.fields['city'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('City'),
max_length=255,
initial=(cartpos.city if cartpos else orderpos.city),
widget=forms.TextInput(attrs={
'autocomplete': 'address-level2',
}),
)
country = (cartpos.country if cartpos else orderpos.country) or guess_country(event)
add_fields['country'] = CountryField(
self.fields['country'] = CountryField(
countries=CachedCountries
).formfield(
required=event.settings.attendee_addresses_required and not self.all_optional,
@@ -329,7 +323,7 @@ class BaseQuestionsForm(forms.Form):
self.data = self.data.copy()
del self.data[fprefix + 'state']
add_fields['state'] = forms.ChoiceField(
self.fields['state'] = forms.ChoiceField(
label=pgettext_lazy('address', 'State'),
required=False,
choices=c,
@@ -337,14 +331,7 @@ class BaseQuestionsForm(forms.Form):
'autocomplete': 'address-level1',
}),
)
add_fields['state'].widget.is_required = True
field_positions = list(
[
(n, event.settings.system_question_order.get(n if n != 'state' else 'country', 0))
for n in add_fields.keys()
]
)
self.fields['state'].widget.is_required = True
for q in questions:
# Do we already have an answer? Provide it as the initial value
@@ -425,17 +412,11 @@ class BaseQuestionsForm(forms.Form):
initial=initial.options.all() if initial else None,
)
elif q.type == Question.TYPE_FILE:
field = ExtFileField(
field = forms.FileField(
label=label, required=required,
help_text=help_text,
initial=initial.file if initial else None,
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
ext_whitelist=(
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
".bmp", ".tif", ".tiff"
),
max_size=10 * 1024 * 1024,
)
elif q.type == Question.TYPE_DATE:
field = forms.DateField(
@@ -497,12 +478,7 @@ class BaseQuestionsForm(forms.Form):
field._required = q.required and not self.all_optional
field.required = False
add_fields['question_%s' % q.id] = field
field_positions.append(('question_%s' % q.id, q.position))
field_positions.sort(key=lambda e: e[1])
for fname, p in field_positions:
self.fields[fname] = add_fields[fname]
self.fields['question_%s' % q.id] = field
responses = question_form_fields.send(sender=event, position=pos)
data = pos.meta_info_data
@@ -583,7 +559,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization',
}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-in-eu': ','.join(EU_COUNTRIES)}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1'}),
'internal_reference': forms.TextInput,
}
labels = {
@@ -633,11 +609,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
)
self.fields['state'].widget.is_required = True
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
if cc and cc not in EU_COUNTRIES and fprefix + 'vat_id' in self.data:
self.data = self.data.copy()
del self.data[fprefix + 'vat_id']
if not event.settings.invoice_address_required or self.all_optional:
for k, f in self.fields.items():
f.required = False
@@ -652,6 +623,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.fields['company'].widget.is_required = True
self.fields['company'].widget.attrs['required'] = 'required'
del self.fields['company'].widget.attrs['data-display-dependency']
if 'vat_id' in self.fields:
del self.fields['vat_id'].widget.attrs['data-display-dependency']
self.fields['name_parts'] = NamePartsFormField(
max_length=255,
@@ -683,9 +656,6 @@ class BaseInvoiceAddressForm(forms.ModelForm):
data = self.cleaned_data
if not data.get('is_business'):
data['company'] = ''
data['vat_id'] = ''
if data.get('is_business') and not data.get('country') in EU_COUNTRIES:
data['vat_id'] = ''
if self.event.settings.invoice_address_required:
if data.get('is_business') and not data.get('company'):
raise ValidationError(_('You need to provide a company name.'))
@@ -706,6 +676,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
) and len(data.get('name_parts', {})) == 1:
# Do not save the country if it is the only field set -- we don't know the user even checked it!
self.cleaned_data['country'] = ''
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and data.get('country') in EU_COUNTRIES and data.get('vat_id'):

View File

@@ -15,7 +15,6 @@ class DatePickerWidget(forms.DateInput):
date_attrs = dict(attrs)
date_attrs.setdefault('class', 'form-control')
date_attrs['class'] += ' datepickerfield'
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
date_attrs['placeholder'] = now().replace(
@@ -33,7 +32,6 @@ class TimePickerWidget(forms.TimeInput):
time_attrs = dict(attrs)
time_attrs.setdefault('class', 'form-control')
time_attrs['class'] += ' timepickerfield'
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
time_attrs['placeholder'] = now().replace(
@@ -104,8 +102,6 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
time_attrs.setdefault('autocomplete', 'off')
date_attrs['class'] += ' datepickerfield'
time_attrs['class'] += ' timepickerfield'
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
def date_placeholder():
df = date_format or get_format('DATE_INPUT_FORMATS')[0]

View File

@@ -391,7 +391,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
p_size = p.wrap(self.event_width, self.event_height)
return txt
if not self.invoice.event.has_subevents or not self.invoice.event.settings.show_dates_on_frontpage:
if not self.invoice.event.has_subevents:
if self.invoice.event.settings.show_date_to and self.invoice.event.date_to:
p_str = (
shorten(self.invoice.event.name) + '\n' +
@@ -672,7 +672,6 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table
]))
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm))
story.append(Paragraph(
pgettext(
@@ -680,7 +679,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
'{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
total=fmt(foreign_total)),
total=fmt(total)),
self.stylesheet['Fineprint']
))

View File

@@ -212,9 +212,8 @@ class SecurityMiddleware(MiddlewareMixin):
# single-sign-on this can be nearly anything so we cannot really restrict
# this. However, we'll restrict it to HTTPS.
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
'report-uri': ["/csp_report/"],
}
if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))

View File

@@ -108,7 +108,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', i18nfield.fields.I18nCharField(max_length=200, verbose_name='Name')),
('slug', models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('currency', models.CharField(default='EUR', max_length=10, verbose_name='Default currency')),
('date_from', models.DateTimeField(verbose_name='Event start time')),
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
@@ -274,7 +274,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('slug', models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
],
options={
'verbose_name': 'Organizer',

View File

@@ -65,7 +65,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', i18nfield.fields.I18nCharField(max_length=200, verbose_name='Name')),
('slug', models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('currency', models.CharField(default='EUR', max_length=10, verbose_name='Default currency')),
('date_from', models.DateTimeField(verbose_name='Event start time')),
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
@@ -229,7 +229,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('slug', models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')),
],
options={
'ordering': ('name',),

View File

@@ -182,12 +182,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='voucher',

View File

@@ -23,12 +23,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Slug'),
),
migrations.AlterField(
model_name='voucher',

View File

@@ -124,7 +124,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AddField(
model_name='requiredaction',
@@ -179,7 +179,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.RunPython(
code=merge_names,

View File

@@ -342,7 +342,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True,
field=models.SlugField(
help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.',
validators=[django.core.validators.RegexValidator(
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),

View File

@@ -33,7 +33,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This will be used in order codes, invoice numbers, links and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AddField(
model_name='requiredaction',

View File

@@ -36,7 +36,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.RunPython(merge_names, migrations.RunPython.noop)
]

View File

@@ -38,7 +38,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='invoice',

View File

@@ -44,7 +44,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.EventSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='eventmetaproperty',
@@ -54,7 +54,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.CreateModel(
name='CheckinList',

View File

@@ -97,7 +97,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.CharField(max_length=50, db_index=True,
field=models.SlugField(
help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be '
'unique among your events. We recommend some kind of abbreviation or a date with less than '
'10 characters that can be easily remembered, but you can also choose to use a random '
@@ -119,7 +119,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True,
field=models.SlugField(
help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can '
'only be used once. This is being used in URLs to refer to your organizer accounts and your'
' events.',

View File

@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='staffsession',

View File

@@ -29,7 +29,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.CharField(max_length=50, db_index=True, help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
field=models.SlugField(help_text='Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used once. This is being used in URLs to refer to your organizer accounts and your events.', unique=True, validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBanlistValidator()], verbose_name='Short form'),
),
migrations.AlterField(
model_name='staffsession',

View File

@@ -1,52 +0,0 @@
# Generated by Django 3.0.5 on 2020-05-11 15:04
import django.db.models.deletion
import django_countries.fields
import jsonfallback.fields
from django.db import migrations, models
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0151_auto_20200421_0737'),
]
operations = [
migrations.AddField(
model_name='checkin',
name='device',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='checkins', to='pretixbase.Device'),
),
migrations.AddField(
model_name='checkin',
name='forced',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='checkin',
name='type',
field=models.CharField(default='entry', max_length=100),
),
migrations.AddField(
model_name='checkinlist',
name='allow_entry_after_exit',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='checkinlist',
name='allow_multiple_entries',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='checkinlist',
name='rules',
field=jsonfallback.fields.FallbackJSONField(default=dict),
),
migrations.AlterUniqueTogether(
name='checkin',
unique_together=set(),
),
]

View File

@@ -1,31 +0,0 @@
# Generated by Django 3.0.6 on 2020-05-28 19:53
import django_countries.fields
from django.db import migrations, models
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0152_auto_20200511_1504'),
]
operations = [
migrations.AddField(
model_name='seat',
name='x',
field=models.FloatField(null=True),
),
migrations.AddField(
model_name='seat',
name='y',
field=models.FloatField(null=True),
),
migrations.AlterField(
model_name='seat',
name='seat_guid',
field=models.CharField(db_index=True, max_length=190),
),
]

View File

@@ -1,31 +0,0 @@
# Generated by Django 3.0.6 on 2020-06-20 16:33
import django_countries.fields
from django.db import migrations, models
import pretix.helpers.countries
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0153_auto_20200528_1953'),
]
operations = [
migrations.AddField(
model_name='subeventitem',
name='disabled',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='subeventitemvariation',
name='disabled',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='invoiceaddress',
name='country',
field=django_countries.fields.CountryField(countries=pretix.helpers.countries.CachedCountries, max_length=2),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.0.6 on 2020-06-26 14:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0154_auto_20200620_1633'),
]
operations = [
migrations.AddField(
model_name='quota',
name='release_after_exit',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.0.6 on 2020-06-28 19:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0155_quota_release_after_exit'),
]
operations = [
migrations.AddField(
model_name='cartposition',
name='override_tax_rate',
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
),
]

View File

@@ -1,9 +1,8 @@
from django.db import models
from django.db.models import Exists, F, Max, OuterRef, Q, Subquery
from django.db.models import Exists, OuterRef
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager
from jsonfallback.fields import FallbackJSONField
from pretix.base.models import LoggedModel
from pretix.base.models.fields import MultiStringField
@@ -20,15 +19,6 @@ class CheckinList(LoggedModel):
default=False,
help_text=_('With this option, people will be able to check in even if the '
'order have not been paid.'))
allow_entry_after_exit = models.BooleanField(
verbose_name=_('Allow re-entering after an exit scan'),
default=True
)
allow_multiple_entries = models.BooleanField(
verbose_name=_('Allow multiple entries per ticket'),
help_text=_('Use this option to turn off warnings if a ticket is scanned a second time.'),
default=False
)
auto_checkin_sales_channels = MultiStringField(
default=[],
@@ -38,7 +28,6 @@ class CheckinList(LoggedModel):
'any of the selected sales channels. This option can be useful when tickets sold at the box office '
'are not checked again before entry and should be considered validated directly upon purchase.')
)
rules = FallbackJSONField(default=dict, blank=True)
objects = ScopedManager(organizer='event__organizer')
@@ -51,43 +40,13 @@ class CheckinList(LoggedModel):
qs = OrderPosition.objects.filter(
order__event=self.event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [
Order.STATUS_PAID],
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [Order.STATUS_PAID],
subevent=self.subevent
)
if self.subevent_id:
qs = qs.filter(subevent_id=self.subevent_id)
if not self.all_products:
qs = qs.filter(item__in=self.limit_products.values_list('id', flat=True))
return qs
@property
def inside_count(self):
return self.positions.annotate(
last_entry=Subquery(
Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.pk,
type=Checkin.TYPE_ENTRY,
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
),
last_exit=Subquery(
Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.pk,
type=Checkin.TYPE_EXIT,
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
),
).filter(
Q(last_entry__isnull=False)
& Q(
Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry'))
)
).count()
@property
def checkin_count(self):
return self.event.cache.get_or_set(
@@ -129,31 +88,20 @@ class CheckinList(LoggedModel):
class Checkin(models.Model):
"""
A check-in object is created when a person enters or exits the event.
A check-in object is created when a person enters the event.
"""
TYPE_ENTRY = 'entry'
TYPE_EXIT = 'exit'
CHECKIN_TYPES = (
(TYPE_ENTRY, _('Entry')),
(TYPE_EXIT, _('Exit')),
)
position = models.ForeignKey('pretixbase.OrderPosition', related_name='checkins', on_delete=models.CASCADE)
datetime = models.DateTimeField(default=now)
nonce = models.CharField(max_length=190, null=True, blank=True)
list = models.ForeignKey(
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
)
type = models.CharField(max_length=100, choices=CHECKIN_TYPES, default=TYPE_ENTRY)
forced = models.BooleanField(default=False)
device = models.ForeignKey(
'pretixbase.Device', related_name='checkins', on_delete=models.PROTECT, null=True, blank=True
)
auto_checked_in = models.BooleanField(default=False)
objects = ScopedManager(organizer='position__order__event__organizer')
class Meta:
ordering = (('-datetime'),)
unique_together = (('list', 'position'),)
def __repr__(self):
return "<Checkin: pos {} on list '{}' at {}>".format(
@@ -161,12 +109,12 @@ class Checkin(models.Model):
)
def save(self, **kwargs):
super().save(**kwargs)
self.position.order.touch()
self.list.event.cache.delete('checkin_count')
self.list.touch()
super().save(**kwargs)
def delete(self, **kwargs):
super().delete(**kwargs)
self.position.order.touch()
super().delete(**kwargs)
self.list.touch()

View File

@@ -12,7 +12,7 @@ from django.core.files.storage import default_storage
from django.core.mail import get_connection
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.template.defaultfilters import date as _date
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
@@ -214,10 +214,8 @@ class EventMixin:
vars_reserved = set()
items_gone = set()
vars_gone = set()
r = getattr(self, '_quota_cache', {})
for q in self.active_quotas:
res = r[q] if q in r else q.availability(allow_cache=True)
res = q.availability(allow_cache=True)
if res[0] == Quota.AVAILABILITY_OK:
if q.active_items:
@@ -287,7 +285,7 @@ class Event(EventMixin, LoggedModel):
max_length=200,
verbose_name=_("Event name"),
)
slug = models.CharField(
slug = models.SlugField(
max_length=50, db_index=True,
help_text=_(
"Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your "
@@ -375,7 +373,6 @@ class Event(EventMixin, LoggedModel):
self.settings.invoice_include_expire_date = True
self.settings.ticketoutput_pdf__enabled = True
self.settings.ticketoutput_passbook__enabled = True
self.settings.event_list_type = 'calendar'
@property
def social_image(self):
@@ -392,17 +389,36 @@ class Event(EventMixin, LoggedModel):
return urljoin(build_absolute_uri(self, 'presale:event.index'), img)
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
from .seating import Seat
qs_annotated = Seat.annotated(self.seats, self.pk, None,
ignore_voucher_id=ignore_voucher.pk if ignore_voucher else None,
minimal_distance=self.settings.seating_minimal_distance,
distance_only_within_row=self.settings.seating_distance_within_row)
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
if self.settings.seating_minimal_distance > 0:
qs = qs.filter(has_closeby_taken=False)
from .orders import CartPosition, Order, OrderPosition
from .vouchers import Voucher
vqs = Voucher.objects.filter(
event=self,
seat_id=OuterRef('pk'),
redeemed__lt=F('max_usages'),
).filter(
Q(valid_until__isnull=True) | Q(valid_until__gte=now())
)
if ignore_voucher:
vqs = vqs.exclude(pk=ignore_voucher.pk)
qs = self.seats.annotate(
has_order=Exists(
OrderPosition.objects.filter(
order__event=self,
seat_id=OuterRef('pk'),
order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
)
),
has_cart=Exists(
CartPosition.objects.filter(
event=self,
seat_id=OuterRef('pk'),
expires__gte=now()
)
),
has_voucher=Exists(
vqs
)
).filter(has_order=False, has_cart=False, has_voucher=False)
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
qs = qs.filter(blocked=False)
return qs
@@ -609,29 +625,12 @@ class Event(EventMixin, LoggedModel):
q.dependency_question = question_map[q.dependency_question_id]
q.save(update_fields=['dependency_question'])
def _walk_rules(rules):
if isinstance(rules, dict):
for k, v in rules.items():
if k == 'lookup':
if v[0] == 'product':
v[1] = str(item_map.get(int(v[1]), 0).pk)
elif v[0] == 'variation':
v[1] = str(variation_map.get(int(v[1]), 0).pk)
else:
_walk_rules(v)
elif isinstance(rules, list):
for i in rules:
_walk_rules(i)
checkin_list_map = {}
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
items = list(cl.limit_products.all())
checkin_list_map[cl.pk] = cl
cl.pk = None
cl.event = self
rules = cl.rules
_walk_rules(rules)
cl.rules = rules
cl.save()
cl.log_action('pretix.object.cloned')
for i in items:
@@ -1043,15 +1042,39 @@ class SubEvent(EventMixin, LoggedModel):
).strip()
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
from .seating import Seat
qs_annotated = Seat.annotated(self.seats, self.event_id, self,
ignore_voucher_id=ignore_voucher.pk if ignore_voucher else None,
minimal_distance=self.settings.seating_minimal_distance,
distance_only_within_row=self.settings.seating_distance_within_row)
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
if self.settings.seating_minimal_distance > 0:
qs = qs.filter(has_closeby_taken=False)
from .orders import CartPosition, Order, OrderPosition
from .vouchers import Voucher
vqs = Voucher.objects.filter(
event_id=self.event_id,
subevent=self,
seat_id=OuterRef('pk'),
redeemed__lt=F('max_usages'),
).filter(
Q(valid_until__isnull=True) | Q(valid_until__gte=now())
)
if ignore_voucher:
vqs = vqs.exclude(pk=ignore_voucher.pk)
qs = self.seats.annotate(
has_order=Exists(
OrderPosition.objects.filter(
order__event_id=self.event_id,
subevent=self,
seat_id=OuterRef('pk'),
order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
)
),
has_cart=Exists(
CartPosition.objects.filter(
event_id=self.event_id,
subevent=self,
seat_id=OuterRef('pk'),
expires__gte=now()
)
),
has_voucher=Exists(
vqs
)
).filter(has_order=False, has_cart=False, has_voucher=False)
if not (sales_channel in self.settings.seating_allow_blocked_seats_for_channel or include_blocked):
qs = qs.filter(blocked=False)
return qs
@@ -1061,35 +1084,21 @@ class SubEvent(EventMixin, LoggedModel):
return self.event.settings
@cached_property
def item_overrides(self):
def item_price_overrides(self):
from .items import SubEventItem
return {
si.item_id: si
for si in SubEventItem.objects.filter(subevent=self)
si.item_id: si.price
for si in SubEventItem.objects.filter(subevent=self, price__isnull=False)
}
@cached_property
def var_overrides(self):
def var_price_overrides(self):
from .items import SubEventItemVariation
return {
si.variation_id: si
for si in SubEventItemVariation.objects.filter(subevent=self)
}
@property
def item_price_overrides(self):
return {
si.item_id: si.price
for si in self.item_overrides.values() if si.price is not None
}
@property
def var_price_overrides(self):
return {
si.variation_id: si.price
for si in self.var_overrides.values() if si.price is not None
for si in SubEventItemVariation.objects.filter(subevent=self, price__isnull=False)
}
@property
@@ -1106,15 +1115,13 @@ class SubEvent(EventMixin, LoggedModel):
return not self.orderposition_set.exists()
def delete(self, *args, **kwargs):
clear_cache = kwargs.pop('clear_cache', False)
super().delete(*args, **kwargs)
if self.event and clear_cache:
if self.event:
self.event.cache.clear()
def save(self, *args, **kwargs):
clear_cache = kwargs.pop('clear_cache', False)
super().save(*args, **kwargs)
if self.event and clear_cache:
if self.event:
self.event.cache.clear()
@staticmethod

View File

@@ -9,10 +9,10 @@ from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import pgettext
from django_countries.fields import CountryField
from django_scopes import ScopedManager
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
from pretix.helpers.countries import FastCountryField
def invoice_filename(instance, filename: str) -> str:
@@ -84,7 +84,7 @@ class Invoice(models.Model):
invoice_from_name = models.CharField(max_length=190, null=True)
invoice_from_zipcode = models.CharField(max_length=190, null=True)
invoice_from_city = models.CharField(max_length=190, null=True)
invoice_from_country = FastCountryField(null=True)
invoice_from_country = CountryField(null=True)
invoice_from_tax_id = models.CharField(max_length=190, null=True)
invoice_from_vat_id = models.CharField(max_length=190, null=True)
invoice_to = models.TextField()
@@ -94,7 +94,7 @@ class Invoice(models.Model):
invoice_to_zipcode = models.CharField(max_length=190, null=True)
invoice_to_city = models.TextField(null=True)
invoice_to_state = models.CharField(max_length=190, null=True)
invoice_to_country = FastCountryField(null=True)
invoice_to_country = CountryField(null=True)
invoice_to_vat_id = models.TextField(null=True)
invoice_to_beneficiary = models.TextField(null=True)
date = models.DateField(default=today)

View File

@@ -7,10 +7,11 @@ from typing import Tuple
import dateutil.parser
import pytz
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Q
from django.db.models import F, Func, Q, Sum
from django.utils import formats
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
@@ -24,6 +25,7 @@ from pretix.base.models import fields
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice
from pretix.base.signals import quota_availability
from .event import Event, SubEvent
@@ -118,7 +120,6 @@ class SubEventItem(models.Model):
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
item = models.ForeignKey('Item', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
disabled = models.BooleanField(default=False)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
@@ -146,7 +147,6 @@ class SubEventItemVariation(models.Model):
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
disabled = models.BooleanField(default=False)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
@@ -448,32 +448,24 @@ class Item(LoggedModel):
return self.event.settings.show_quota_left
return self.show_quota_left
def tax(self, price=None, base_price_is='auto', currency=None, invoice_address=None, override_tax_rate=None, include_bundled=False):
def tax(self, price=None, base_price_is='auto', currency=None, include_bundled=False):
price = price if price is not None else self.default_price
if not self.tax_rule:
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
rate=Decimal('0.00'), name='')
else:
t = self.tax_rule.tax(price, base_price_is=base_price_is, invoice_address=invoice_address,
override_tax_rate=override_tax_rate, currency=currency or self.event.currency)
t = self.tax_rule.tax(price, base_price_is=base_price_is,
currency=currency or self.event.currency)
if include_bundled:
for b in self.bundles.all():
if b.designated_price and b.bundled_item.tax_rule_id != self.tax_rule_id:
if b.bundled_variation:
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross',
invoice_address=invoice_address,
currency=currency)
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
else:
bprice = b.bundled_item.tax(b.designated_price * b.count,
invoice_address=invoice_address,
base_price_is='gross',
currency=currency)
compare_price = self.tax_rule.tax(b.designated_price * b.count,
override_tax_rate=override_tax_rate,
invoice_address=invoice_address,
currency=currency)
bprice = b.bundled_item.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
compare_price = self.tax_rule.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
t.net += bprice.net - compare_price.net
t.tax += bprice.tax - compare_price.tax
t.name = "MIXED!"
@@ -543,8 +535,6 @@ class Item(LoggedModel):
quotacounter[q] += b.count
for q, n in quotacounter.items():
if n == 0:
continue
a = q.availability(count_waitinglist=count_waitinglist, _cache=_cache)
if a[1] is None:
continue
@@ -681,31 +671,23 @@ class ItemVariation(models.Model):
def price(self):
return self.default_price if self.default_price is not None else self.item.default_price
def tax(self, price=None, base_price_is='auto', currency=None, include_bundled=False, override_tax_rate=None,
invoice_address=None):
def tax(self, price=None, base_price_is='auto', currency=None, include_bundled=False):
price = price if price is not None else self.price
if not self.item.tax_rule:
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
rate=Decimal('0.00'), name='')
else:
t = self.item.tax_rule.tax(price, base_price_is=base_price_is, currency=currency,
override_tax_rate=override_tax_rate,
invoice_address=invoice_address)
t = self.item.tax_rule.tax(price, base_price_is=base_price_is, currency=currency)
if include_bundled:
for b in self.item.bundles.all():
if b.designated_price and b.bundled_item.tax_rule_id != self.item.tax_rule_id:
if b.bundled_variation:
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross',
currency=currency,
invoice_address=invoice_address)
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
else:
bprice = b.bundled_item.tax(b.designated_price * b.count, base_price_is='gross',
currency=currency,
invoice_address=invoice_address)
compare_price = self.item.tax_rule.tax(b.designated_price * b.count, base_price_is='gross',
currency=currency, invoice_address=invoice_address)
bprice = b.bundled_item.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
compare_price = self.item.tax_rule.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
t.net += bprice.net - compare_price.net
t.tax += bprice.tax - compare_price.tax
t.name = "MIXED!"
@@ -1351,16 +1333,6 @@ class Quota(LoggedModel):
)
closed = models.BooleanField(default=False)
release_after_exit = models.BooleanField(
verbose_name=_('Allow to sell more tickets once people have checked out'),
help_text=_('With this option, quota will be released as soon as people are scanned at an exit of your event. '
'This will only happen if they have been scanned both at an entry and at an exit and the exit '
'is the more recent scan. It does not matter which check-in list either of the scans was on, '
'but check-in lists are ignored if they are set to "Allow re-entering after an exit scan" to '
'prevent accidental overbooking.'),
default=False,
)
objects = ScopedManager(organizer='event__organizer')
class Meta:
@@ -1378,7 +1350,6 @@ class Quota(LoggedModel):
self.event.cache.clear()
def save(self, *args, **kwargs):
# This is *not* called when the db-level cache is upated, since we use bulk_update there
clear_cache = kwargs.pop('clear_cache', True)
super().save(*args, **kwargs)
if self.event and clear_cache:
@@ -1413,8 +1384,6 @@ class Quota(LoggedModel):
:returns: a tuple where the first entry is one of the ``Quota.AVAILABILITY_`` constants
and the second is the number of available tickets.
"""
from ..services.quotas import QuotaAvailability
if allow_cache and self.cache_is_hot() and count_waitinglist:
return self.cached_availability_state, self.cached_availability_number
@@ -1423,16 +1392,141 @@ class Quota(LoggedModel):
if _cache is not None and self.pk in _cache:
return _cache[self.pk]
qa = QuotaAvailability(count_waitinglist=count_waitinglist, early_out=False)
qa.queue(self)
qa.compute(now_dt=now_dt)
res = qa.results[self]
now_dt = now_dt or now()
res = self._availability(now_dt, count_waitinglist)
for recv, resp in quota_availability.send(sender=self.event, quota=self, result=res,
count_waitinglist=count_waitinglist):
res = resp
if res[0] <= Quota.AVAILABILITY_ORDERED and self.close_when_sold_out and not self.closed:
self.closed = True
self.save(update_fields=['closed'])
self.log_action('pretix.event.quota.closed')
self.event.cache.delete('item_quota_cache')
rewrite_cache = count_waitinglist and (
not self.cache_is_hot(now_dt) or res[0] > self.cached_availability_state
)
if rewrite_cache:
self.cached_availability_state = res[0]
self.cached_availability_number = res[1]
self.cached_availability_time = now_dt
if self.size is None:
self.cached_availability_paid_orders = self.count_paid_orders()
self.save(
update_fields=[
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
'cached_availability_paid_orders'
],
clear_cache=False,
using='default'
)
if _cache is not None:
_cache[self.pk] = res
_cache['_count_waitinglist'] = count_waitinglist
return res
def _availability(self, now_dt: datetime=None, count_waitinglist=True, ignore_closed=False):
now_dt = now_dt or now()
if self.closed and not ignore_closed:
return Quota.AVAILABILITY_ORDERED, 0
size_left = self.size
if size_left is None:
return Quota.AVAILABILITY_OK, None
paid_orders = self.count_paid_orders()
self.cached_availability_paid_orders = paid_orders
size_left -= paid_orders
if size_left <= 0:
return Quota.AVAILABILITY_GONE, 0
size_left -= self.count_pending_orders()
if size_left <= 0:
return Quota.AVAILABILITY_ORDERED, 0
size_left -= self.count_blocking_vouchers(now_dt)
if size_left <= 0:
return Quota.AVAILABILITY_ORDERED, 0
if count_waitinglist:
size_left -= self.count_waiting_list_pending()
if size_left <= 0:
return Quota.AVAILABILITY_ORDERED, 0
size_left -= self.count_in_cart(now_dt)
if size_left <= 0:
return Quota.AVAILABILITY_RESERVED, 0
return Quota.AVAILABILITY_OK, size_left
def count_blocking_vouchers(self, now_dt: datetime=None) -> int:
from pretix.base.models import Voucher
now_dt = now_dt or now()
if 'sqlite3' in settings.DATABASES['default']['ENGINE']:
func = 'MAX'
else: # NOQA
func = 'GREATEST'
return Voucher.objects.filter(
Q(event=self.event) & Q(subevent=self.subevent) &
Q(block_quota=True) &
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now_dt)) &
Q(Q(self._position_lookup) | Q(quota=self))
).values('id').aggregate(
free=Sum(Func(F('max_usages') - F('redeemed'), 0, function=func))
)['free'] or 0
def count_waiting_list_pending(self) -> int:
from pretix.base.models import WaitingListEntry
return WaitingListEntry.objects.filter(
Q(voucher__isnull=True) & Q(subevent=self.subevent) &
self._position_lookup
).distinct().count()
def count_in_cart(self, now_dt: datetime=None) -> int:
from pretix.base.models import CartPosition
now_dt = now_dt or now()
return CartPosition.objects.filter(
Q(event=self.event) & Q(subevent=self.subevent) &
Q(expires__gte=now_dt) &
Q(
Q(voucher__isnull=True)
| Q(voucher__block_quota=False)
| Q(voucher__valid_until__lt=now_dt)
) &
self._position_lookup
).count()
def count_pending_orders(self) -> dict:
from pretix.base.models import Order, OrderPosition
# This query has beeen benchmarked against a Count('id', distinct=True) aggregate and won by a small margin.
return OrderPosition.objects.filter(
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event, subevent=self.subevent
).count()
def count_paid_orders(self):
from pretix.base.models import Order, OrderPosition
return OrderPosition.objects.filter(
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event, subevent=self.subevent
).count()
@cached_property
def _position_lookup(self) -> Q:
return (
( # Orders for items which do not have any variations
Q(variation__isnull=True) &
Q(item_id__in=Quota.items.through.objects.filter(quota_id=self.pk).values_list('item_id', flat=True))
) | ( # Orders for items which do have any variations
Q(variation__in=Quota.variations.through.objects.filter(quota_id=self.pk).values_list('itemvariation_id', flat=True))
)
)
class QuotaExceededException(Exception):
pass
@@ -1441,6 +1535,7 @@ class Quota(LoggedModel):
for variation in (variations or []):
if variation.item not in items:
raise ValidationError(_('All variations must belong to an item contained in the items list.'))
break
@staticmethod
def clean_items(event, items, variations):

View File

@@ -22,7 +22,7 @@ class LogEntry(models.Model):
in the database. This uses django.contrib.contenttypes to allow a
relation to an arbitrary database object.
:param datetime: The timestamp of the logged action
:param datatime: The timestamp of the logged action
:type datetime: datetime
:param user: The user that performed the action
:type user: User

View File

@@ -27,7 +27,7 @@ from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries.fields import Country
from django_countries.fields import Country, CountryField
from django_scopes import ScopedManager, scopes_disabled
from i18nfield.strings import LazyI18nString
from jsonfallback.fields import FallbackJSONField
@@ -44,7 +44,7 @@ from pretix.base.services.locking import NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.countries import CachedCountries
from .base import LockModel, LoggedModel
from .event import Event, SubEvent
from .items import Item, ItemVariation, Question, QuestionOption, Quota
@@ -502,16 +502,10 @@ class Order(LockModel, LoggedModel):
# Algorithm to choose which payments are to be refunded to create the least hassle
payments = payments or self.payments.filter(state=OrderPayment.PAYMENT_STATE_CONFIRMED)
for p in payments:
if p.payment_provider:
p.full_refund_possible = p.payment_provider.payment_refund_supported(p)
p.partial_refund_possible = p.payment_provider.payment_partial_refund_supported(p)
p.propose_refund = Decimal('0.00')
p.available_amount = p.amount - p.refunded_amount
else:
p.full_refund_possible = False
p.partial_refund_possible = False
p.propose_refund = Decimal('0.00')
p.available_amount = Decimal('0.00')
p.full_refund_possible = p.payment_provider.payment_refund_supported(p)
p.partial_refund_possible = p.payment_provider.payment_partial_refund_supported(p)
p.propose_refund = Decimal('0.00')
p.available_amount = p.amount - p.refunded_amount
unused_payments = set(p for p in payments if p.full_refund_possible or p.partial_refund_possible)
to_refund = amount
@@ -795,7 +789,7 @@ class Order(LockModel, LoggedModel):
only be attached for this position and child positions, the link will only point to the
position and the attendee email will be used if available.
"""
from pretix.base.services.mail import SendMailException, mail, render_mail, TolerantDict
from pretix.base.services.mail import SendMailException, mail, render_mail
if not self.email:
return
@@ -810,7 +804,6 @@ class Order(LockModel, LoggedModel):
try:
email_content = render_mail(template, context)
subject = subject.format_map(TolerantDict(context))
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
@@ -1077,7 +1070,7 @@ class AbstractPosition(models.Model):
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True, null=True)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=True, null=True)
country = FastCountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True)
country = CountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True)
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True, null=True)
class Meta:
@@ -1172,33 +1165,6 @@ class AbstractPosition(models.Model):
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
return scheme['concatenation'](self.attendee_name_parts).strip()
@property
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return sd.name
return self.state
@property
def state_for_address(self):
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
if not self.state or str(self.country) not in COUNTRIES_WITH_STATE_IN_ADDRESS:
return ""
if COUNTRIES_WITH_STATE_IN_ADDRESS[str(self.country)][1] == 'long':
return self.state_name
return self.state
def address_format(self):
lines = [
self.attendee_name,
self.company,
self.street,
(self.zipcode or '') + ' ' + (self.city or '') + ' ' + (self.state_for_address or ''),
self.country.name
]
lines = [r.strip() for r in lines if r]
return '\n'.join(lines).strip()
class OrderPayment(models.Model):
"""
@@ -1801,10 +1767,7 @@ class OrderFee(models.Model):
self.fee_type, self.value
)
def _calculate_tax(self, tax_rule=None):
if tax_rule:
self.tax_rule = tax_rule
def _calculate_tax(self):
try:
ia = self.order.invoice_address
except InvoiceAddress.DoesNotExist:
@@ -1814,9 +1777,13 @@ class OrderFee(models.Model):
self.tax_rule = self.order.event.settings.tax_rate_default
if self.tax_rule:
tax = self.tax_rule.tax(self.value, base_price_is='gross', invoice_address=ia)
self.tax_rate = tax.rate
self.tax_value = tax.tax
if self.tax_rule.tax_applicable(ia):
tax = self.tax_rule.tax(self.value, base_price_is='gross')
self.tax_rate = tax.rate
self.tax_value = tax.tax
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
@@ -1956,16 +1923,20 @@ class OrderPosition(AbstractPosition):
self.item.id, self.variation.id if self.variation else 0, self.order_id
)
def _calculate_tax(self, tax_rule=None):
self.tax_rule = tax_rule or self.item.tax_rule
def _calculate_tax(self):
self.tax_rule = self.item.tax_rule
try:
ia = self.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
if self.tax_rule:
tax = self.tax_rule.tax(self.price, invoice_address=ia, base_price_is='gross')
self.tax_rate = tax.rate
self.tax_value = tax.tax
if self.tax_rule.tax_applicable(ia):
tax = self.tax_rule.tax(self.price, base_price_is='gross')
self.tax_rate = tax.rate
self.tax_value = tax.tax
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
@@ -2104,10 +2075,6 @@ class CartPosition(AbstractPosition):
includes_tax = models.BooleanField(
default=True
)
override_tax_rate = models.DecimalField(
max_digits=10, decimal_places=2,
null=True, blank=True
)
is_bundled = models.BooleanField(default=False)
objects = ScopedManager(organizer='event__organizer')
@@ -2124,8 +2091,6 @@ class CartPosition(AbstractPosition):
@property
def tax_rate(self):
if self.includes_tax:
if self.override_tax_rate is not None:
return self.override_tax_rate
return self.item.tax(self.price, base_price_is='gross').rate
else:
return Decimal('0.00')
@@ -2133,7 +2098,7 @@ class CartPosition(AbstractPosition):
@property
def tax_value(self):
if self.includes_tax:
return self.item.tax(self.price, override_tax_rate=self.override_tax_rate, base_price_is='gross').tax
return self.item.tax(self.price, base_price_is='gross').tax
else:
return Decimal('0.00')
@@ -2149,8 +2114,8 @@ class InvoiceAddress(models.Model):
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
country = FastCountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
countries=CachedCountries)
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
countries=CachedCountries)
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
help_text=_('Only for business customers within the EU.'))

View File

@@ -32,7 +32,7 @@ class Organizer(LoggedModel):
settings_namespace = 'organizer'
name = models.CharField(max_length=200,
verbose_name=_("Name"))
slug = models.CharField(
slug = models.SlugField(
max_length=50, db_index=True,
help_text=_(
"Should be short, only contain lowercase letters, numbers, dots, and dashes. Every slug can only be used "

View File

@@ -5,8 +5,7 @@ import jsonschema
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Exists, F, OuterRef, Q, Value
from django.db.models.functions import Power
from django.db.models import F, Q
from django.utils.deconstruct import deconstructible
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _
@@ -42,7 +41,7 @@ class SeatingPlan(LoggedModel):
layout = models.TextField(validators=[SeatingPlanLayoutValidator()])
Category = namedtuple('Categrory', 'name')
RawSeat = namedtuple('Seat', 'name guid number row category zone sorting_rank row_label seat_label x y')
RawSeat = namedtuple('Seat', 'name guid number row category zone sorting_rank row_label seat_label')
def __str__(self):
return self.name
@@ -70,9 +69,7 @@ class SeatingPlan(LoggedModel):
# *will* have gaps. We chose this way over just sorting the seats and continuously enumerating them as an
# optimization, because this way we do not need to update the rank of very seat if we change a plan a little.
for zi, z in enumerate(self.layout_data['zones']):
zpos = (z['position']['x'], z['position']['y'])
for ri, r in enumerate(z['rows']):
rpos = (zpos[0] + r['position']['x'], zpos[1] + r['position']['y'])
row_label = None
if r.get('row_label'):
row_label = r['row_label'].replace("%s", r.get('row_number', str(ri)))
@@ -101,9 +98,7 @@ class SeatingPlan(LoggedModel):
seat_label=seat_label,
zone=z['name'],
category=s['category'],
sorting_rank=rank,
x=rpos[0] + s['position']['x'],
y=rpos[1] + s['position']['y'],
sorting_rank=rank
)
@@ -135,8 +130,6 @@ class Seat(models.Model):
product = models.ForeignKey('Item', null=True, blank=True, related_name='seats', on_delete=models.CASCADE)
blocked = models.BooleanField(default=False)
sorting_rank = models.BigIntegerField(default=0)
x = models.FloatField(null=True)
y = models.FloatField(null=True)
class Meta:
ordering = ['sorting_rank', 'seat_guid']
@@ -160,68 +153,7 @@ class Seat(models.Model):
return self.name
return ', '.join(parts)
@classmethod
def annotated(cls, qs, event_id, subevent, ignore_voucher_id=None, minimal_distance=0,
ignore_order_id=None, ignore_cart_id=None, distance_only_within_row=False):
from . import Order, OrderPosition, Voucher, CartPosition
vqs = Voucher.objects.filter(
event_id=event_id,
subevent=subevent,
seat_id=OuterRef('pk'),
redeemed__lt=F('max_usages'),
).filter(
Q(valid_until__isnull=True) | Q(valid_until__gte=now())
)
if ignore_voucher_id:
vqs = vqs.exclude(pk=ignore_voucher_id)
opqs = OrderPosition.objects.filter(
order__event_id=event_id,
subevent=subevent,
seat_id=OuterRef('pk'),
order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
)
if ignore_order_id:
opqs = opqs.exclude(order_id=ignore_order_id)
cqs = CartPosition.objects.filter(
event_id=event_id,
subevent=subevent,
seat_id=OuterRef('pk'),
expires__gte=now()
)
if ignore_cart_id:
cqs = cqs.exclude(cart_id=ignore_cart_id)
qs_annotated = qs.annotate(
has_order=Exists(
opqs
),
has_cart=Exists(
cqs
),
has_voucher=Exists(
vqs
)
)
if minimal_distance > 0:
# TODO: Is there a more performant implementation on PostgreSQL using
# https://www.postgresql.org/docs/8.2/functions-geometry.html ?
sq_closeby = qs_annotated.annotate(
distance=(
Power(F('x') - OuterRef('x'), Value(2), output_field=models.FloatField()) +
Power(F('y') - OuterRef('y'), Value(2), output_field=models.FloatField())
)
).filter(
Q(has_order=True) | Q(has_cart=True) | Q(has_voucher=True),
distance__lt=minimal_distance ** 2
)
if distance_only_within_row:
sq_closeby = sq_closeby.filter(row_name=OuterRef('row_name'))
qs_annotated = qs_annotated.annotate(has_closeby_taken=Exists(sq_closeby))
return qs_annotated
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None, sales_channel='web',
ignore_distancing=False, distance_ignore_cart_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 and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
@@ -241,32 +173,4 @@ class Seat(models.Model):
opqs = opqs.exclude(pk=ignore_orderpos.pk)
if ignore_voucher_id:
vqs = vqs.exclude(pk=ignore_voucher_id)
if opqs.exists() or (ignore_cart is not True and cpqs.exists()) or vqs.exists():
return False
if self.event.settings.seating_minimal_distance > 0 and not ignore_distancing:
ev = (self.subevent or self.event)
qs_annotated = Seat.annotated(ev.seats, self.event_id, self.subevent,
ignore_voucher_id=ignore_voucher_id,
minimal_distance=0,
ignore_order_id=ignore_orderpos.order_id if ignore_orderpos else None,
ignore_cart_id=(
distance_ignore_cart_id or
(ignore_cart.cart_id if ignore_cart else None)
))
qs_closeby_taken = qs_annotated.annotate(
distance=(
Power(F('x') - Value(self.x), Value(2), output_field=models.FloatField()) +
Power(F('y') - Value(self.y), Value(2), output_field=models.FloatField())
)
).exclude(pk=self.pk).filter(
Q(has_order=True) | Q(has_cart=True) | Q(has_voucher=True),
distance__lt=self.event.settings.seating_minimal_distance ** 2
)
if self.event.settings.seating_distance_within_row:
qs_closeby_taken = qs_closeby_taken.filter(row_name=self.row_name)
if qs_closeby_taken.exists():
return False
return True
return not opqs.exists() and (ignore_cart is True or not cpqs.exists()) and not vqs.exists()

View File

@@ -5,12 +5,12 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.utils.formats import localize
from django.utils.translation import gettext_lazy as _
from django_countries.fields import CountryField
from i18nfield.fields import I18nCharField
from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
from pretix.base.templatetags.money import money_filter
from pretix.helpers.countries import FastCountryField
class TaxedPrice:
@@ -116,7 +116,7 @@ class TaxRule(LoggedModel):
"ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax "
"calculation. USE AT YOUR OWN RISK.")
)
home_country = FastCountryField(
home_country = CountryField(
verbose_name=_('Merchant country'),
blank=True,
help_text=_('Your country of residence. This is the country the EU reverse charge rule will not apply in, '
@@ -164,39 +164,16 @@ class TaxRule(LoggedModel):
def has_custom_rules(self):
return self.custom_rules and self.custom_rules != '[]'
def tax_rate_for(self, invoice_address):
if not self._tax_applicable(invoice_address):
return Decimal('0.00')
if self.has_custom_rules:
rule = self.get_matching_rule(invoice_address)
if rule.get('action', 'vat') == 'vat' and rule.get('rate') is not None:
return Decimal(rule.get('rate'))
return Decimal(self.rate)
def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, invoice_address=None,
subtract_from_gross=Decimal('0.00')):
def tax(self, base_price, base_price_is='auto', currency=None):
from .event import Event
try:
currency = currency or self.event.currency
except Event.DoesNotExist:
pass
rate = Decimal(self.rate)
if override_tax_rate is not None:
rate = override_tax_rate
elif invoice_address:
adjust_rate = self.tax_rate_for(invoice_address)
if adjust_rate != rate:
normal_price = self.tax(base_price, base_price_is, currency, subtract_from_gross=subtract_from_gross)
base_price = normal_price.net
base_price_is = 'net'
subtract_from_gross = Decimal('0.00')
rate = adjust_rate
if rate == Decimal('0.00'):
if self.rate == Decimal('0.00'):
return TaxedPrice(
net=base_price - subtract_from_gross, gross=base_price - subtract_from_gross, tax=Decimal('0.00'),
rate=rate, name=self.name
net=base_price, gross=base_price, tax=Decimal('0.00'),
rate=self.rate, name=self.name
)
if base_price_is == 'auto':
@@ -206,22 +183,19 @@ class TaxRule(LoggedModel):
base_price_is = 'net'
if base_price_is == 'gross':
gross = max(Decimal('0.00'), base_price - subtract_from_gross)
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
gross = base_price
net = round_decimal(gross - (base_price * (1 - 100 / (100 + self.rate))),
currency)
elif base_price_is == 'net':
net = base_price
gross = round_decimal((net * (1 + rate / 100)), currency)
if subtract_from_gross:
gross -= subtract_from_gross
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
currency)
gross = round_decimal((net * (1 + self.rate / 100)),
currency)
else:
raise ValueError('Unknown base price type: {}'.format(base_price_is))
return TaxedPrice(
net=net, gross=gross, tax=gross - net,
rate=rate, name=self.name
rate=self.rate, name=self.name
)
@property
@@ -269,7 +243,7 @@ class TaxRule(LoggedModel):
return False
def _tax_applicable(self, invoice_address):
def tax_applicable(self, invoice_address):
if self._custom_rules:
rule = self.get_matching_rule(invoice_address)
return rule.get('action', 'vat') == 'vat'

View File

@@ -18,6 +18,7 @@ from django.template.loader import get_template
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_countries import Countries
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
@@ -33,7 +34,6 @@ from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text
from pretix.helpers.countries import CachedCountries
from pretix.helpers.money import DecimalTextInput
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
from pretix.presale.views import get_cart, get_cart_total
@@ -286,7 +286,7 @@ class BasePaymentProvider:
('_restricted_countries',
forms.MultipleChoiceField(
label=_('Restrict to countries'),
choices=CachedCountries(),
choices=Countries(),
help_text=_('Only allow choosing this payment provider for invoice addresses in the selected '
'countries. If you don\'t select any country, all countries are allowed. This is only '
'enabled if the invoice address is required.'),
@@ -306,7 +306,7 @@ class BasePaymentProvider:
initial=['web'],
widget=forms.CheckboxSelectMultiple,
help_text=_(
'Only allow the usage of this payment provider in the selected sales channels.'),
'Only allow the usage of this payment provider in the following sales channels'),
)),
('_hidden',
forms.BooleanField(
@@ -901,16 +901,16 @@ class ManualPayment(BasePaymentProvider):
('email_instructions', I18nFormField(
label=_('Payment process description in order confirmation emails'),
help_text=_('This text will be included for the {payment_info} placeholder in order confirmation '
'mails. It should instruct the user on how to proceed with the payment. You can use '
'the placeholders {order}, {total}, {currency} and {total_with_currency}.'),
'mails. It should instruct the user on how to proceed with the payment. You can use'
'the placeholders {order}, {total}, {currency} and {total_with_currency}'),
widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
)),
('pending_description', I18nFormField(
label=_('Payment process description for pending orders'),
help_text=_('This text will be shown on the order confirmation page for pending orders. '
'It should instruct the user on how to proceed with the payment. You can use '
'the placeholders {order}, {total}, {currency} and {total_with_currency}.'),
'It should instruct the user on how to proceed with the payment. You can use'
'the placeholders {order}, {total}, {currency} and {total_with_currency}'),
widget=I18nTextarea,
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
)),
@@ -1199,7 +1199,7 @@ class GiftCardPayment(BasePaymentProvider):
if not gc.accepted_by(self.event.organizer): # noqa - just a safeguard
raise PaymentException(_("This gift card is not accepted by this event organizer."))
if payment.amount > gc.value: # noqa - just a safeguard
raise PaymentException(_("This gift card was used in the meantime. Please try again."))
raise PaymentException(_("This gift card was used in the meantime. Please try again"))
if gc.expires and gc.expires < now(): # noqa - just a safeguard
messages.error(request, _("This gift card is no longer valid."))
return

View File

@@ -109,23 +109,6 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
}),
("attendee_company", {
"label": _("Attendee company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: op.company or (op.addon_to.company if op.addon_to else '')
}),
('attendee_address', {
'label': _('Full attendee address'),
'editor_sample': _('John Doe\nSample company\nSesame Street 42\n12345 Any City\nAtlantis'),
'evaluate': lambda op, order, event: op.address_format()
}),
("attendee_country", {
"label": _("Attendee country"),
"editor_sample": 'Atlantis',
"evaluate": lambda op, order, ev: str(getattr(op.country, 'name', '')) or (
str(getattr(op.addon_to.country, 'name', '')) if op.addon_to else ''
)
}),
("event_name", {
"label": _("Event name"),
"editor_sample": _("Sample event name"),
@@ -222,6 +205,11 @@ DEFAULT_VARIABLES = OrderedDict((
"editor_sample": _("Sample city"),
"evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else ''
}),
("attendee_company", {
"label": _("Attendee company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: op.company or (op.addon_to.company if op.addon_to else '')
}),
("addons", {
"label": _("List of Add-Ons"),
"editor_sample": _("Add-on 1\nAdd-on 2"),

View File

@@ -81,8 +81,7 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
logger.exception('Order canceled email could not be sent to attendee')
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,),
acks_late=True)
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_fixed: str,
keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False,
send: bool=False, send_subject: dict=None, send_message: dict=None,

View File

@@ -25,7 +25,6 @@ from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.checkin import _save_answers
from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.pricing import get_price
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import validate_cart_addons
@@ -253,12 +252,6 @@ class CartManager:
if self._sales_channel not in op.item.sales_channels:
raise CartError(error_messages['unavailable'])
if op.subevent and op.item.pk in op.subevent.item_overrides and op.subevent.item_overrides[op.item.pk].disabled:
raise CartError(error_messages['not_for_sale'])
if op.subevent and op.variation and op.variation.pk in op.subevent.var_overrides and op.subevent.var_overrides[op.variation.pk].disabled:
raise CartError(error_messages['not_for_sale'])
if op.item.has_variations and not op.variation:
raise CartError(error_messages['not_for_sale'])
@@ -735,14 +728,10 @@ class CartManager:
def _get_quota_availability(self):
quotas_ok = defaultdict(int)
qa = QuotaAvailability()
qa.queue(*[k for k, v in self._quota_diff.items() if v > 0])
qa.compute(now_dt=self.now_dt)
for quota, count in self._quota_diff.items():
if count <= 0:
quotas_ok[quota] = 0
break
avail = qa.results[quota]
avail = quota.availability(self.now_dt)
if avail[1] is not None and avail[1] < count:
quotas_ok[quota] = min(count, avail[1])
else:
@@ -895,9 +884,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,
sales_channel=self._sales_channel,
distance_ignore_cart_id=self.cart_id):
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']
@@ -907,7 +894,6 @@ class CartManager:
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,
override_tax_rate=op.price.rate,
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:
@@ -941,7 +927,7 @@ class CartManager:
new_cart_positions.append(CartPosition(
event=self.event, item=b.item, variation=b.variation,
price=b.price.gross, expires=self._expiry, cart_id=self.cart_id,
voucher=None, addon_to=cp, override_tax_rate=b.price.rate,
voucher=None, addon_to=cp,
subevent=b.subevent, includes_tax=b.includes_tax, is_bundled=True
))
@@ -1033,15 +1019,19 @@ def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress
for pos in positions:
if not pos.item.tax_rule:
continue
rate = pos.item.tax_rule.tax_rate_for(invoice_address)
if pos.tax_rate != rate:
current_net = pos.price - pos.tax_value
new_gross = pos.item.tax(current_net, base_price_is='net', invoice_address=invoice_address).gross
pos.price = new_gross
pos.includes_tax = rate != Decimal('0.00')
pos.override_tax_rate = rate
pos.save(update_fields=['price', 'includes_tax', 'override_tax_rate'])
charge_tax = pos.item.tax_rule.tax_applicable(invoice_address)
if pos.includes_tax and not charge_tax:
price = pos.item.tax(pos.price, base_price_is='gross').net
totaldiff += price - pos.price
pos.price = price
pos.includes_tax = False
pos.save(update_fields=['price', 'includes_tax'])
elif charge_tax and not pos.includes_tax:
price = pos.item.tax(pos.price, base_price_is='net').gross
totaldiff += price - pos.price
pos.price = price
pos.includes_tax = True
pos.save(update_fields=['price', 'includes_tax'])
return totaldiff
@@ -1089,14 +1079,23 @@ def get_fees(event, request, total, invoice_address, provider, positions):
if payment_fee:
payment_fee_tax_rule = event.settings.tax_rate_default or TaxRule.zero()
payment_fee_tax = payment_fee_tax_rule.tax(payment_fee, base_price_is='gross', invoice_address=invoice_address)
fees.append(OrderFee(
fee_type=OrderFee.FEE_TYPE_PAYMENT,
value=payment_fee,
tax_rate=payment_fee_tax.rate,
tax_value=payment_fee_tax.tax,
tax_rule=payment_fee_tax_rule
))
if payment_fee_tax_rule.tax_applicable(invoice_address):
payment_fee_tax = payment_fee_tax_rule.tax(payment_fee, base_price_is='gross')
fees.append(OrderFee(
fee_type=OrderFee.FEE_TYPE_PAYMENT,
value=payment_fee,
tax_rate=payment_fee_tax.rate,
tax_value=payment_fee_tax.tax,
tax_rule=payment_fee_tax_rule
))
else:
fees.append(OrderFee(
fee_type=OrderFee.FEE_TYPE_PAYMENT,
value=payment_fee,
tax_rate=Decimal('0.00'),
tax_value=Decimal('0.00'),
tax_rule=payment_fee_tax_rule
))
return fees

View File

@@ -1,87 +1,13 @@
from datetime import timedelta
import dateutil
from django.db import transaction
from django.db.models.functions import TruncDate
from django.db.models import Prefetch
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.timezone import now, override
from django.utils.timezone import now
from django.utils.translation import gettext as _
from pretix.base.models import (
Checkin, CheckinList, Device, Order, OrderPosition, QuestionOption,
Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption,
)
from pretix.base.signals import checkin_created, order_placed
from pretix.helpers.jsonlogic import Logic
def get_logic_environment(ev):
def build_time(t=None, value=None):
if t == "custom":
return dateutil.parser.parse(value)
elif t == 'date_from':
return ev.date_from
elif t == 'date_to':
return ev.date_to
elif t == 'date_admission':
return ev.date_admission or ev.date_from
def is_before(t1, t2, tolerance=None):
if tolerance:
return t1 < t2 + timedelta(minutes=float(tolerance))
else:
return t1 < t2
logic = Logic()
logic.add_operation('objectList', lambda *objs: list(objs))
logic.add_operation('lookup', lambda model, pk, str: int(pk))
logic.add_operation('inList', lambda a, b: a in b)
logic.add_operation('buildTime', build_time)
logic.add_operation('isBefore', is_before)
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
return logic
class LazyRuleVars:
def __init__(self, position, clist, dt):
self._position = position
self._clist = clist
self._dt = dt
def __getitem__(self, item):
if item[0] != '_' and hasattr(self, item):
return getattr(self, item)
raise KeyError()
@property
def now(self):
return self._dt
@property
def product(self):
return self._position.item_id
@property
def variation(self):
return self._position.variation_id
@cached_property
def entries_number(self):
return self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist).count()
@cached_property
def entries_today(self):
tz = self._clist.event.timezone
midnight = now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
return self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__gte=midnight).count()
@cached_property
def entries_days(self):
tz = self._clist.event.timezone
with override(tz):
return self._position.checkins.filter(list=self._clist, type=Checkin.TYPE_ENTRY).annotate(
day=TruncDate('datetime')
).values('day').distinct().count()
class CheckInError(Exception):
@@ -136,7 +62,7 @@ def _save_answers(op, answers, given_answers):
@transaction.atomic
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY):
user=None, auth=None, canceled_supported=False):
"""
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
not valid at this time.
@@ -153,11 +79,18 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
"""
dt = datetime or now()
# Lock order positions
op = OrderPosition.all.select_for_update().get(pk=op.pk)
checkin_questions = list(
clist.event.questions.filter(ask_during_checkin=True, items__in=[op.item_id])
)
# Fetch order position with related objects
op = OrderPosition.all.select_related(
'item', 'variation', 'order', 'addon_to'
).prefetch_related(
'item__questions',
Prefetch(
'item__questions',
queryset=Question.objects.filter(ask_during_checkin=True),
to_attr='checkin_questions'
),
'answers'
).get(pk=op.pk)
if op.canceled or op.order.status not in (Order.STATUS_PAID, Order.STATUS_PENDING):
raise CheckInError(
@@ -165,25 +98,19 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'canceled' if canceled_supported else 'unpaid'
)
answers = {a.question: a for a in op.answers.all()}
require_answers = []
if checkin_questions:
answers = {a.question: a for a in op.answers.all()}
for q in checkin_questions:
if q not in given_answers and q not in answers:
require_answers.append(q)
for q in op.item.checkin_questions:
if q not in given_answers and q not in answers:
require_answers.append(q)
_save_answers(op, answers, given_answers)
_save_answers(op, answers, given_answers)
if not clist.all_products and op.item_id not in [i.pk for i in clist.limit_products.all()]:
raise CheckInError(
_('This order position has an invalid product for this check-in list.'),
'product'
)
elif clist.subevent_id and op.subevent_id != clist.subevent_id:
raise CheckInError(
_('This order position has an invalid date for this check-in list.'),
'product'
)
elif op.order.status != Order.STATUS_PAID and not force and not (
ignore_unpaid and clist.include_pending and op.order.status == Order.STATUS_PENDING
):
@@ -197,56 +124,40 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
'incomplete',
require_answers
)
else:
try:
ci, created = Checkin.objects.get_or_create(position=op, list=clist, defaults={
'datetime': dt,
'nonce': nonce,
})
except Checkin.MultipleObjectsReturned:
ci, created = Checkin.objects.filter(position=op, list=clist).last(), False
if type == Checkin.TYPE_ENTRY and clist.rules and not force:
rule_data = LazyRuleVars(op, clist, dt)
logic = get_logic_environment(op.subevent or clist.event)
if not logic.apply(clist.rules, rule_data):
if created or (nonce and nonce == ci.nonce):
if created:
op.order.log_action('pretix.event.checkin', data={
'position': op.id,
'positionid': op.positionid,
'first': True,
'forced': op.order.status != Order.STATUS_PAID,
'datetime': dt,
'list': clist.pk
}, user=user, auth=auth)
checkin_created.send(op.order.event, checkin=ci)
else:
if not force:
raise CheckInError(
_('This entry is not permitted due to custom rules.'),
'rules'
_('This ticket has already been redeemed.'),
'already_redeemed',
)
device = None
if isinstance(auth, Device):
device = auth
last_ci = op.checkins.order_by('-datetime').filter(list=clist).only('type', 'nonce').first()
entry_allowed = (
type == Checkin.TYPE_EXIT or
clist.allow_multiple_entries or
last_ci is None or
(clist.allow_entry_after_exit and last_ci.type == Checkin.TYPE_EXIT)
)
if nonce and ((last_ci and last_ci.nonce == nonce) or op.checkins.filter(type=type, list=clist, device=device, nonce=nonce).exists()):
return
if entry_allowed or force:
ci = Checkin.objects.create(
position=op,
type=type,
list=clist,
datetime=dt,
device=device,
nonce=nonce,
forced=force and not entry_allowed,
)
op.order.log_action('pretix.event.checkin', data={
'position': op.id,
'positionid': op.positionid,
'first': True,
'forced': force or op.order.status != Order.STATUS_PAID,
'first': False,
'forced': force,
'datetime': dt,
'type': type,
'list': clist.pk
}, user=user, auth=auth)
checkin_created.send(op.order.event, checkin=ci)
else:
raise CheckInError(
_('This ticket has already been redeemed.'),
'already_redeemed',
)
@receiver(order_placed, dispatch_uid="autocheckin_order_placed")
@@ -261,6 +172,5 @@ def order_placed(sender, **kwargs):
for op in order.positions.all():
for cl in cls:
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
if not cl.subevent_id or cl.subevent_id == op.subevent_id:
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True)
checkin_created.send(event, checkin=ci)
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True)
checkin_created.send(event, checkin=ci)

View File

@@ -31,9 +31,9 @@ def clean_cached_files(sender, **kwargs):
@receiver(signal=periodic_task)
@scopes_disabled()
def clean_cached_tickets(sender, **kwargs):
for cf in CachedTicket.objects.filter(created__lte=now() - timedelta(days=3)):
for cf in CachedTicket.objects.filter(created__lte=now() - timedelta(days=30)):
cf.delete()
for cf in CachedCombinedTicket.objects.filter(created__lte=now() - timedelta(days=3)):
for cf in CachedCombinedTicket.objects.filter(created__lte=now() - timedelta(days=30)):
cf.delete()
for cf in CachedTicket.objects.filter(created__lte=now() - timedelta(minutes=30), file__isnull=True):
cf.delete()

Some files were not shown because too many files have changed in this diff Show More