mirror of
https://github.com/pretix/pretix.git
synced 2026-06-27 03:56:15 +00:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f8e6a2a4b | |||
| c084b91ab3 | |||
| 8baaa0a8c6 | |||
| 53070f5d4b | |||
| 5685a349ea | |||
| 1af69d5c76 | |||
| adddc7a71e | |||
| 11f23c3fd2 | |||
| 954fece6cf | |||
| 8ef6adc3d5 | |||
| 88ba7ab53a | |||
| eae55e4b5a | |||
| 5ae839f62e | |||
| 7314d32422 | |||
| 97d6ae8e55 | |||
| 13063cb9d2 | |||
| 2792813d95 | |||
| d6aeefdf09 | |||
| 13056ef477 | |||
| 6e2b5eae9a | |||
| 4cfb10b254 | |||
| ebd336e8cb | |||
| 1357b010de | |||
| 09b2e69178 | |||
| 5e34032821 | |||
| 46cee890f0 | |||
| 4a2ac110b3 | |||
| 7eefd3dc59 | |||
| fdca62685c | |||
| 7ae38b5e97 | |||
| 76e9093fea | |||
| b3c9dca024 | |||
| f4710cf019 | |||
| 5f192fd0ce | |||
| a897f60fc5 | |||
| 74107781ce | |||
| ad219df7cf | |||
| 002ab4aa06 | |||
| a84a726185 | |||
| 5f58b93c71 | |||
| 3eaaf80c0a | |||
| 3b5d811b27 | |||
| f0da2b7233 | |||
| d8d7440b52 | |||
| a1ec9fceb0 | |||
| 27ff73255b | |||
| bba103156c | |||
| f1a98b5c30 | |||
| 405b3a22e1 | |||
| a51c2a36a6 | |||
| 8e00970f04 | |||
| 8ca2fe7707 | |||
| b93e2307d0 | |||
| 97f3b72254 | |||
| 00a77d3de9 | |||
| 35d9a0dacf | |||
| d2e6320e1e | |||
| 671eb902a8 | |||
| be67059099 | |||
| 6e3791a49e | |||
| e3bd665093 | |||
| 748e2bb2fa | |||
| b13b34f00d | |||
| 641e3216d9 | |||
| c70901c129 | |||
| 460d39b8c2 | |||
| a9963aead1 |
@@ -60,6 +60,10 @@ Here is the currently recommended set of commands::
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_ia_company
|
||||
ON pretixbase_invoiceaddress
|
||||
USING gin (upper("company") gin_trgm_ops);
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_email_upper
|
||||
ON public.pretixbase_orderposition (upper((attendee_email)::text));
|
||||
CREATE INDEX CONCURRENTLY pretix_addidx_voucher_code_upper
|
||||
ON public.pretixbase_voucher (upper((code)::text));
|
||||
|
||||
|
||||
Also, if you use our ``pretix-shipping`` plugin::
|
||||
|
||||
@@ -15,8 +15,24 @@ number string Invoice number
|
||||
order string Order code of the order this invoice belongs to
|
||||
is_cancellation boolean ``true``, if this invoice is the cancellation of a
|
||||
different invoice.
|
||||
invoice_from string Sender address
|
||||
invoice_to string Receiver address
|
||||
invoice_from_name string Sender address: Name
|
||||
invoice_from string Sender address: Address lines
|
||||
invoice_from_zipcode string Sender address: ZIP code
|
||||
invoice_from_city string Sender address: City
|
||||
invoice_from_country string Sender address: Country code
|
||||
invoice_from_tax_id string Sender address: Local Tax ID
|
||||
invoice_from_vat_id string Sender address: EU VAT ID
|
||||
invoice_to string Full recipient address
|
||||
invoice_to_company string Recipient address: Company name
|
||||
invoice_to_name string Recipient address: Person name
|
||||
invoice_to_street string Recipient address: Address lines
|
||||
invoice_to_zipcode string Recipient address: ZIP code
|
||||
invoice_to_city string Recipient address: City
|
||||
invoice_to_state string Recipient address: State (only used in some countries)
|
||||
invoice_to_country string Recipient address: Country code
|
||||
invoice_to_vat_id string Recipient address: EU VAT ID
|
||||
invoice_to_beneficiary string Invoice beneficiary
|
||||
custom_field string Custom invoice address field
|
||||
date date Invoice date
|
||||
refers string Invoice number of an invoice this invoice refers to
|
||||
(for example a cancellation refers to the invoice it
|
||||
@@ -30,6 +46,31 @@ footer_text string Text to be prin
|
||||
lines list of objects The actual invoice contents
|
||||
├ position integer Number of the line within an invoice.
|
||||
├ description string Text representing the invoice line (e.g. product name)
|
||||
├ item integer Product used to create this line. Note that everything
|
||||
about the product might have changed since the creation
|
||||
of the invoice. Can be ``null`` for all invoice lines
|
||||
created before this field was introduced as well as for
|
||||
all lines not created by a product (e.g. a shipping or
|
||||
cancellation fee).
|
||||
├ variation integer Product variation used to create this line. Note that everything
|
||||
about the product might have changed since the creation
|
||||
of the invoice. Can be ``null`` for all invoice lines
|
||||
created before this field was introduced as well as for
|
||||
all lines not created by a product (e.g. a shipping or
|
||||
cancellation fee).
|
||||
├ event_date_from datetime Start date of the (sub)event this line was created for as it
|
||||
was set during invoice creation. Can be ``null`` for all invoice
|
||||
lines created before this was introduced as well as for lines in
|
||||
an event series not created by a product (e.g. shipping or
|
||||
cancellation fees).
|
||||
├ event_date_to datetime End date of the (sub)event this line was created for as it
|
||||
was set during invoice creation. Can be ``null`` for all invoice
|
||||
lines created before this was introduced as well as for lines in
|
||||
an event series not created by a product (e.g. shipping or
|
||||
cancellation fees) as well as whenever the respective (sub)event
|
||||
has no end date set.
|
||||
├ attendee_name string Attendee name at time of invoice creation. Can be ``null`` if no
|
||||
name was set or if names are configured to not be added to invoices.
|
||||
├ gross_value money (string) Price including taxes
|
||||
├ tax_value money (string) Tax amount included
|
||||
├ tax_name string Name of used tax rate (e.g. "VAT")
|
||||
@@ -50,6 +91,12 @@ internal_reference string Customer's refe
|
||||
|
||||
The attribute ``lines.number`` has been added.
|
||||
|
||||
.. versionchanged:: 3.17
|
||||
|
||||
The attribute ``invoice_to_*``, ``invoice_from_*``, ``custom_field``, ``lines.item``, ``lines.variation``, ``lines.event_date_from``,
|
||||
``lines.event_date_to``, and ``lines.attendee_name`` have been added.
|
||||
``refers`` now returns an invoice number including the prefix.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -83,8 +130,24 @@ Endpoints
|
||||
"number": "SAMPLECONF-00001",
|
||||
"order": "ABC12",
|
||||
"is_cancellation": false,
|
||||
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
|
||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT ID: EU123456789",
|
||||
"invoice_from_name": "Big Events LLC",
|
||||
"invoice_from": "Demo street 12",
|
||||
"invoice_from_zipcode":"",
|
||||
"invoice_from_city":"Demo town",
|
||||
"invoice_from_country":"US",
|
||||
"invoice_from_tax_id":"",
|
||||
"invoice_from_vat_id":"",
|
||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
||||
"invoice_to_company": "Sample company",
|
||||
"invoice_to_name": "John Doe",
|
||||
"invoice_to_street": "Test street 12",
|
||||
"invoice_to_zipcode": "12345",
|
||||
"invoice_to_city": "Testington",
|
||||
"invoice_to_state": null,
|
||||
"invoice_to_country": "TE",
|
||||
"invoice_to_vat_id": "EU123456789",
|
||||
"invoice_to_beneficiary": "",
|
||||
"custom_field": null,
|
||||
"date": "2017-12-01",
|
||||
"refers": null,
|
||||
"locale": "en",
|
||||
@@ -97,6 +160,11 @@ Endpoints
|
||||
{
|
||||
"position": 1,
|
||||
"description": "Budget Ticket",
|
||||
"item": 1234,
|
||||
"variation": 245,
|
||||
"event_date_from": "2017-12-27T10:00:00Z",
|
||||
"event_date_to": null,
|
||||
"attendee_name": null,
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_name": "VAT",
|
||||
@@ -148,8 +216,24 @@ Endpoints
|
||||
"number": "SAMPLECONF-00001",
|
||||
"order": "ABC12",
|
||||
"is_cancellation": false,
|
||||
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
|
||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT ID: EU123456789",
|
||||
"invoice_from_name": "Big Events LLC",
|
||||
"invoice_from": "Demo street 12",
|
||||
"invoice_from_zipcode":"",
|
||||
"invoice_from_city":"Demo town",
|
||||
"invoice_from_country":"US",
|
||||
"invoice_from_tax_id":"",
|
||||
"invoice_from_vat_id":"",
|
||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
||||
"invoice_to_company": "Sample company",
|
||||
"invoice_to_name": "John Doe",
|
||||
"invoice_to_street": "Test street 12",
|
||||
"invoice_to_zipcode": "12345",
|
||||
"invoice_to_city": "Testington",
|
||||
"invoice_to_state": null,
|
||||
"invoice_to_country": "TE",
|
||||
"invoice_to_vat_id": "EU123456789",
|
||||
"invoice_to_beneficiary": "",
|
||||
"custom_field": null,
|
||||
"date": "2017-12-01",
|
||||
"refers": null,
|
||||
"locale": "en",
|
||||
@@ -162,6 +246,11 @@ Endpoints
|
||||
{
|
||||
"position": 1,
|
||||
"description": "Budget Ticket",
|
||||
"item": 1234,
|
||||
"variation": 245,
|
||||
"event_date_from": "2017-12-27T10:00:00Z",
|
||||
"event_date_to": null,
|
||||
"attendee_name": null,
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_name": "VAT",
|
||||
|
||||
@@ -13,7 +13,10 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the waiting list entry
|
||||
created datetime Creation date of the waiting list entry
|
||||
name string Name of the user on the waiting list (or ``null``)
|
||||
name_parts object of strings Decomposition of name of the user (or ``null``)
|
||||
email string Email address of the user on the waiting list
|
||||
phone string Phone number of the user on the waiting list (or ``null``)
|
||||
voucher integer Internal ID of the voucher sent to this user. If
|
||||
this field is set, the user has been sent a voucher
|
||||
and is no longer waiting. If it is ``null``, the
|
||||
|
||||
+7
-2
@@ -6,8 +6,8 @@ localecompile:
|
||||
./manage.py compilemessages
|
||||
|
||||
localegen:
|
||||
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" --ignore "pretix/static/npm_dir/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
|
||||
|
||||
staticfiles: jsi18n
|
||||
./manage.py collectstatic --noinput
|
||||
@@ -23,3 +23,8 @@ test:
|
||||
|
||||
coverage:
|
||||
coverage run -m py.test
|
||||
|
||||
npminstall:
|
||||
mkdir -p pretix/static.dist/node_prefix
|
||||
npm install --prefix=pretix/static.dist/node_prefix pretix/static/npm_dir/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class FullAccessSecurityProfile:
|
||||
|
||||
|
||||
class AllowListSecurityProfile:
|
||||
allowlist = tuple()
|
||||
allowlist = ()
|
||||
|
||||
def is_allowed(self, request):
|
||||
key = (request.method, f"{request.resolver_match.namespace}:{request.resolver_match.url_name}")
|
||||
@@ -95,6 +95,8 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:taxrule-list'),
|
||||
('GET', 'api-v1:ticketlayout-list'),
|
||||
('GET', 'api-v1:ticketlayoutitem-list'),
|
||||
('GET', 'api-v1:badgelayout-list'),
|
||||
('GET', 'api-v1:badgeitem-list'),
|
||||
('GET', 'api-v1:order-list'),
|
||||
('POST', 'api-v1:order-list'),
|
||||
('GET', 'api-v1:order-detail'),
|
||||
|
||||
@@ -309,7 +309,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
|
||||
# Item Meta properties
|
||||
if item_meta_properties is not None:
|
||||
current = [imp for imp in event.item_meta_properties.all()]
|
||||
current = list(event.item_meta_properties.all())
|
||||
for key, value in item_meta_properties.items():
|
||||
prop = self.item_meta_props.get(key)
|
||||
if prop in current:
|
||||
@@ -614,6 +614,11 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'waiting_list_enabled',
|
||||
'waiting_list_hours',
|
||||
'waiting_list_auto',
|
||||
'waiting_list_names_asked',
|
||||
'waiting_list_names_required',
|
||||
'waiting_list_phones_asked',
|
||||
'waiting_list_phones_required',
|
||||
'waiting_list_phones_explanation_text',
|
||||
'max_items_per_order',
|
||||
'reservation_time',
|
||||
'contact_mail',
|
||||
@@ -624,6 +629,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'frontpage_subevent_ordering',
|
||||
'event_list_type',
|
||||
'frontpage_text',
|
||||
'event_info_text',
|
||||
'attendee_names_asked',
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
|
||||
@@ -18,18 +18,18 @@ class FormFieldWrapperField(serializers.Field):
|
||||
|
||||
|
||||
simple_mappings = (
|
||||
(forms.DateField, serializers.DateField, tuple()),
|
||||
(forms.TimeField, serializers.TimeField, tuple()),
|
||||
(forms.SplitDateTimeField, serializers.DateTimeField, tuple()),
|
||||
(forms.DateTimeField, serializers.DateTimeField, tuple()),
|
||||
(forms.DateField, serializers.DateField, ()),
|
||||
(forms.TimeField, serializers.TimeField, ()),
|
||||
(forms.SplitDateTimeField, serializers.DateTimeField, ()),
|
||||
(forms.DateTimeField, serializers.DateTimeField, ()),
|
||||
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
|
||||
(forms.FloatField, serializers.FloatField, tuple()),
|
||||
(forms.IntegerField, serializers.IntegerField, tuple()),
|
||||
(forms.EmailField, serializers.EmailField, tuple()),
|
||||
(forms.UUIDField, serializers.UUIDField, tuple()),
|
||||
(forms.URLField, serializers.URLField, tuple()),
|
||||
(forms.NullBooleanField, serializers.NullBooleanField, tuple()),
|
||||
(forms.BooleanField, serializers.BooleanField, tuple()),
|
||||
(forms.FloatField, serializers.FloatField, ()),
|
||||
(forms.IntegerField, serializers.IntegerField, ()),
|
||||
(forms.EmailField, serializers.EmailField, ()),
|
||||
(forms.UUIDField, serializers.UUIDField, ()),
|
||||
(forms.URLField, serializers.URLField, ()),
|
||||
(forms.NullBooleanField, serializers.NullBooleanField, ()),
|
||||
(forms.BooleanField, serializers.BooleanField, ()),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -45,6 +45,14 @@ class CompatibleCountryField(serializers.Field):
|
||||
return instance.country_old
|
||||
|
||||
|
||||
class CountryField(serializers.Field):
|
||||
def to_internal_value(self, data):
|
||||
return {self.field_name: Country(data)}
|
||||
|
||||
def to_representation(self, src):
|
||||
return str(src) if src else None
|
||||
|
||||
|
||||
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||
country = CompatibleCountryField(source='*')
|
||||
name = serializers.CharField(required=False)
|
||||
@@ -1322,17 +1330,24 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InvoiceLine
|
||||
fields = ('position', 'description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
|
||||
fields = ('position', 'description', 'item', 'variation', 'attendee_name', 'event_date_from',
|
||||
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
|
||||
|
||||
|
||||
class InvoiceSerializer(I18nAwareModelSerializer):
|
||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
||||
refers = serializers.SlugRelatedField(slug_field='invoice_no', read_only=True)
|
||||
refers = serializers.SlugRelatedField(slug_field='full_invoice_no', read_only=True)
|
||||
lines = InlineInvoiceLineSerializer(many=True)
|
||||
invoice_to_country = CountryField()
|
||||
invoice_from_country = CountryField()
|
||||
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
|
||||
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
|
||||
'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
|
||||
'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode',
|
||||
'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary',
|
||||
'custom_field', 'date', 'refers', 'locale',
|
||||
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
|
||||
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
|
||||
'internal_reference')
|
||||
|
||||
@@ -8,7 +8,7 @@ class WaitingListSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = WaitingListEntry
|
||||
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
|
||||
fields = ('id', 'created', 'name', 'name_parts', 'email', 'phone', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
|
||||
read_only_fields = ('id', 'created', 'voucher')
|
||||
|
||||
def validate(self, data):
|
||||
@@ -32,4 +32,11 @@ class WaitingListSerializer(I18nAwareModelSerializer):
|
||||
if availability[0] == 100:
|
||||
raise ValidationError("This product is currently available.")
|
||||
|
||||
if data.get('name') and data.get('name_parts'):
|
||||
raise ValidationError(
|
||||
{'name': ['Do not specify name if you specified name_parts.']}
|
||||
)
|
||||
if data.get('name_parts') and '_scheme' not in data.get('name_parts'):
|
||||
data['name_parts']['_scheme'] = event.settings.name_scheme
|
||||
|
||||
return data
|
||||
|
||||
@@ -53,8 +53,8 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class InitializeView(APIView):
|
||||
authentication_classes = tuple()
|
||||
permission_classes = tuple()
|
||||
authentication_classes = ()
|
||||
permission_classes = ()
|
||||
|
||||
def post(self, request, format=None):
|
||||
serializer = InitializationRequestSerializer(data=request.data)
|
||||
|
||||
@@ -2,10 +2,12 @@ import inspect
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from itertools import groupby
|
||||
from smtplib import SMTPResponseException
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.db.models import Count
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.timezone import now
|
||||
@@ -128,9 +130,21 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
|
||||
if order:
|
||||
htmlctx['order'] = order
|
||||
positions = list(order.positions.select_related(
|
||||
'item', 'variation', 'subevent', 'addon_to'
|
||||
).annotate(
|
||||
has_addons=Count('addons')
|
||||
))
|
||||
htmlctx['cart'] = [(k, list(v)) for k, v in groupby(
|
||||
positions, key=lambda op: (
|
||||
op.item, op.variation, op.subevent, op.attendee_name,
|
||||
(op.pk if op.addon_to_id else None), (op.pk if op.has_addons else None)
|
||||
)
|
||||
)]
|
||||
|
||||
if position:
|
||||
htmlctx['position'] = position
|
||||
htmlctx['ev'] = position.subevent or self.event
|
||||
|
||||
tpl = get_template(self.template_name)
|
||||
body_html = inline_css(tpl.render(htmlctx))
|
||||
@@ -237,6 +251,8 @@ def get_email_context(**kwargs):
|
||||
from pretix.base.models import InvoiceAddress
|
||||
|
||||
event = kwargs['event']
|
||||
if 'position' in kwargs:
|
||||
kwargs.setdefault("position_or_address", kwargs['position'])
|
||||
if 'order' in kwargs:
|
||||
try:
|
||||
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
||||
|
||||
@@ -13,12 +13,12 @@ from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext
|
||||
|
||||
from pretix.base.models import Invoice, InvoiceLine, OrderPayment
|
||||
from ..services.export import ExportError
|
||||
|
||||
from ...control.forms.filter import get_all_payment_providers
|
||||
from ...helpers import GroupConcat
|
||||
from ...helpers.iter import chunked_iterable
|
||||
from ..exporter import BaseExporter, MultiSheetListExporter
|
||||
from ..services.export import ExportError
|
||||
from ..services.invoices import invoice_pdf_task
|
||||
from ..signals import (
|
||||
register_data_exporters, register_multievent_data_exporters,
|
||||
|
||||
@@ -691,13 +691,18 @@ class QuotaListExporter(ListExporter):
|
||||
verbose_name = gettext_lazy('Quota availabilities')
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
has_subevents = self.event.has_subevents
|
||||
headers = [
|
||||
_('Quota name'), _('Total quota'), _('Paid orders'), _('Pending orders'), _('Blocking vouchers'),
|
||||
_('Current user\'s carts'), _('Waiting list'), _('Exited orders'), _('Current availability')
|
||||
]
|
||||
if has_subevents:
|
||||
headers.append(pgettext('subevent', 'Date'))
|
||||
headers.append(_('Start date'))
|
||||
headers.append(_('End date'))
|
||||
yield headers
|
||||
|
||||
quotas = list(self.event.quotas.all())
|
||||
quotas = list(self.event.quotas.select_related('subevent'))
|
||||
qa = QuotaAvailability(full_results=True)
|
||||
qa.queue(*quotas)
|
||||
qa.compute()
|
||||
@@ -715,6 +720,18 @@ class QuotaListExporter(ListExporter):
|
||||
qa.count_exited_orders[quota],
|
||||
_('Infinite') if avail[1] is None else avail[1]
|
||||
]
|
||||
if has_subevents:
|
||||
if quota.subevent:
|
||||
row.append(quota.subevent.name)
|
||||
row.append(quota.subevent.date_from.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
if quota.subevent.date_to:
|
||||
row.append(quota.subevent.date_to.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
else:
|
||||
row.append('')
|
||||
else:
|
||||
row.append('')
|
||||
row.append('')
|
||||
row.append('')
|
||||
yield row
|
||||
|
||||
def get_filename(self):
|
||||
|
||||
@@ -82,7 +82,9 @@ class WaitingListExporter(ListExporter):
|
||||
|
||||
headers = [
|
||||
_('Date'),
|
||||
_('Name'),
|
||||
_('Email'),
|
||||
_('Phone number'),
|
||||
_('Product name'),
|
||||
_('Variation'),
|
||||
_('Event slug'),
|
||||
@@ -117,7 +119,9 @@ class WaitingListExporter(ListExporter):
|
||||
|
||||
row = [
|
||||
entry.created.astimezone(tz).strftime(datetime_format), # alternative: .isoformat(),
|
||||
entry.name,
|
||||
entry.email,
|
||||
entry.phone,
|
||||
str(entry.item) if entry.item else "",
|
||||
str(entry.variation) if entry.variation else "",
|
||||
entry.event.slug,
|
||||
|
||||
@@ -100,7 +100,7 @@ class NamePartsWidget(forms.MultiWidget):
|
||||
if not isinstance(value, list):
|
||||
value = self.decompress(value)
|
||||
output = []
|
||||
final_attrs = self.build_attrs(attrs or dict())
|
||||
final_attrs = self.build_attrs(attrs or {})
|
||||
if 'required' in final_attrs:
|
||||
del final_attrs['required']
|
||||
id_ = final_attrs.get('id', None)
|
||||
@@ -122,6 +122,8 @@ class NamePartsWidget(forms.MultiWidget):
|
||||
these_attrs.pop('data-no-required-attr', None)
|
||||
these_attrs['autocomplete'] = (self.attrs.get('autocomplete', '') + ' ' + self.autofill_map.get(self.scheme['fields'][i][0], 'off')).strip()
|
||||
these_attrs['data-size'] = self.scheme['fields'][i][2]
|
||||
if len(self.widgets) > 1:
|
||||
these_attrs['aria-label'] = self.scheme['fields'][i][1]
|
||||
else:
|
||||
these_attrs = final_attrs
|
||||
output.append(widget.render(name + '_%s' % i, widget_value, these_attrs, renderer=renderer))
|
||||
@@ -220,7 +222,7 @@ class WrappedPhonePrefixSelect(Select):
|
||||
country_name = locale.territories.get(country_code)
|
||||
if country_name:
|
||||
choices.append((prefix, "{} {}".format(country_name, prefix)))
|
||||
super().__init__(choices=sorted(choices, key=lambda item: item[1]))
|
||||
super().__init__(choices=sorted(choices, key=lambda item: item[1]), attrs={'aria-label': pgettext_lazy('phonenumber', 'International area code')})
|
||||
|
||||
def render(self, name, value, *args, **kwargs):
|
||||
return super().render(name, value or self.initial, *args, **kwargs)
|
||||
@@ -243,7 +245,10 @@ class WrappedPhonePrefixSelect(Select):
|
||||
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
||||
|
||||
def __init__(self, attrs=None, initial=None):
|
||||
widgets = (WrappedPhonePrefixSelect(initial), forms.TextInput())
|
||||
attrs = {
|
||||
'aria-label': pgettext_lazy('phonenumber', 'Phone number (without international area code)')
|
||||
}
|
||||
widgets = (WrappedPhonePrefixSelect(initial), forms.TextInput(attrs=attrs))
|
||||
super(PhoneNumberPrefixWidget, self).__init__(widgets, attrs)
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
|
||||
@@ -445,7 +445,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
if self.invoice.custom_field:
|
||||
story.append(Paragraph(
|
||||
'{}: {}'.format(
|
||||
bleach.clean(self.invoice.event.settings.invoice_address_custom_field, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
bleach.clean(str(self.invoice.event.settings.invoice_address_custom_field), tags=[]).strip().replace('\n', '<br />\n'),
|
||||
bleach.clean(self.invoice.custom_field, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
),
|
||||
self.stylesheet['Normal']
|
||||
@@ -475,7 +475,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if self.invoice.introductory_text:
|
||||
story.append(Paragraph(
|
||||
bleach.clean(self.invoice.introductory_text, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
self.invoice.introductory_text,
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
story.append(Spacer(1, 10 * mm))
|
||||
@@ -578,13 +578,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if self.invoice.payment_provider_text:
|
||||
story.append(Paragraph(
|
||||
bleach.clean(self.invoice.payment_provider_text, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
self.invoice.payment_provider_text,
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.additional_text:
|
||||
story.append(Paragraph(
|
||||
bleach.clean(self.invoice.additional_text, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
self.invoice.additional_text,
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
story.append(Spacer(1, 15 * mm))
|
||||
|
||||
@@ -16,6 +16,8 @@ class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
|
||||
'(dotted path, comma separation)')
|
||||
parser.add_argument('--exclude', action='store', type=str, help='Exclude the tasks with this name '
|
||||
'(dotted path, comma separation)')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
verbosity = int(options['verbosity'])
|
||||
@@ -28,6 +30,9 @@ class Command(BaseCommand):
|
||||
if options.get('tasks'):
|
||||
if name not in options.get('tasks').split(','):
|
||||
continue
|
||||
if options.get('exclude'):
|
||||
if name in options.get('exclude').split(','):
|
||||
continue
|
||||
|
||||
if verbosity > 1:
|
||||
self.stdout.write(f'INFO Running {name}…')
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 3.0.10 on 2021-03-01 15:10
|
||||
|
||||
import jsonfallback.fields
|
||||
import phonenumber_field.modelfields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0176_auto_20210205_1512'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='waitinglistentry',
|
||||
name='name_cached',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='waitinglistentry',
|
||||
name='name_parts',
|
||||
field=jsonfallback.fields.FallbackJSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='waitinglistentry',
|
||||
name='phone',
|
||||
field=phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.0.12 on 2021-03-08 13:26
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0177_auto_20210301_1510'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invoiceline',
|
||||
name='attendee_name',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceline',
|
||||
name='event_date_to',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceline',
|
||||
name='item',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Item'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceline',
|
||||
name='variation',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.ItemVariation'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
# Generated by Django 3.0.10 on 2021-03-11 16:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def clean_duplicates(apps, schema_editor):
|
||||
while True:
|
||||
delete_options = """
|
||||
DELETE
|
||||
FROM pretixbase_questionanswer_options
|
||||
WHERE questionanswer_id IN (
|
||||
SELECT MIN(qa.id)
|
||||
FROM pretixbase_questionanswer qa
|
||||
GROUP BY qa.cartposition_id, qa.orderposition_id, qa.question_id
|
||||
HAVING COUNT(*) > 1
|
||||
);
|
||||
"""
|
||||
delete_answers = """
|
||||
DELETE
|
||||
FROM pretixbase_questionanswer
|
||||
WHERE pretixbase_questionanswer.id IN (
|
||||
SELECT MIN(qa.id)
|
||||
FROM pretixbase_questionanswer qa
|
||||
GROUP BY qa.cartposition_id, qa.orderposition_id, qa.question_id
|
||||
HAVING COUNT(*) > 1
|
||||
);
|
||||
"""
|
||||
with schema_editor.connection.cursor() as cursor:
|
||||
cursor.execute(delete_options)
|
||||
cursor.execute(delete_answers)
|
||||
if cursor.rowcount == 0:
|
||||
return
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0178_auto_20210308_1326'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
clean_duplicates,
|
||||
migrations.RunPython.noop,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='questionanswer',
|
||||
unique_together={('orderposition', 'question'), ('cartposition', 'question')},
|
||||
),
|
||||
]
|
||||
@@ -697,9 +697,9 @@ class Event(EventMixin, LoggedModel):
|
||||
for k, v in rules.items():
|
||||
if k == 'lookup':
|
||||
if v[0] == 'product':
|
||||
v[1] = str(item_map.get(int(v[1]), 0).pk)
|
||||
v[1] = str(item_map.get(int(v[1]), 0).pk) if int(v[1]) in item_map else "0"
|
||||
elif v[0] == 'variation':
|
||||
v[1] = str(variation_map.get(int(v[1]), 0).pk)
|
||||
v[1] = str(variation_map.get(int(v[1]), 0).pk) if int(v[1]) in variation_map else "0"
|
||||
else:
|
||||
_walk_rules(v)
|
||||
elif isinstance(rules, list):
|
||||
|
||||
@@ -273,6 +273,14 @@ class InvoiceLine(models.Model):
|
||||
:type subevent: SubEvent
|
||||
:param event_date_from: Event date of the (sub)event at the time the invoice was created
|
||||
:type event_date_from: datetime
|
||||
:param event_date_to: Event end date of the (sub)event at the time the invoice was created
|
||||
:type event_date_to: datetime
|
||||
:param item: The item this line refers to
|
||||
:type item: Item
|
||||
:param variation: The variation this line refers to
|
||||
:type variation: ItemVariation
|
||||
:param attendee_name: The attendee name at the time the invoice was created
|
||||
:type attendee_name: str
|
||||
"""
|
||||
invoice = models.ForeignKey('Invoice', related_name='lines', on_delete=models.CASCADE)
|
||||
position = models.PositiveIntegerField(default=0)
|
||||
@@ -283,6 +291,10 @@ class InvoiceLine(models.Model):
|
||||
tax_name = models.CharField(max_length=190)
|
||||
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
||||
event_date_from = models.DateTimeField(null=True)
|
||||
event_date_to = models.DateTimeField(null=True)
|
||||
item = models.ForeignKey('Item', null=True, blank=True, on_delete=models.PROTECT)
|
||||
variation = models.ForeignKey('ItemVariation', null=True, blank=True, on_delete=models.PROTECT)
|
||||
attendee_name = models.TextField(null=True, blank=True)
|
||||
|
||||
@property
|
||||
def net_value(self):
|
||||
|
||||
@@ -983,6 +983,9 @@ class QuestionAnswer(models.Model):
|
||||
|
||||
objects = ScopedManager(organizer='question__event__organizer')
|
||||
|
||||
class Meta:
|
||||
unique_together = [['orderposition', 'question'], ['cartposition', 'question']]
|
||||
|
||||
@property
|
||||
def backend_file_url(self):
|
||||
if self.file:
|
||||
|
||||
@@ -5,11 +5,14 @@ from django.db import models, transaction
|
||||
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 phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Voucher
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
|
||||
from .base import LoggedModel
|
||||
from .event import Event, SubEvent
|
||||
@@ -37,9 +40,21 @@ class WaitingListEntry(LoggedModel):
|
||||
verbose_name=_("On waiting list since"),
|
||||
auto_now_add=True
|
||||
)
|
||||
name_cached = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Name"),
|
||||
blank=True, null=True,
|
||||
)
|
||||
name_parts = FallbackJSONField(
|
||||
blank=True, default=dict
|
||||
)
|
||||
email = models.EmailField(
|
||||
verbose_name=_("E-mail address")
|
||||
)
|
||||
phone = PhoneNumberField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Phone number")
|
||||
)
|
||||
voucher = models.ForeignKey(
|
||||
'Voucher',
|
||||
verbose_name=_("Assigned voucher"),
|
||||
@@ -83,6 +98,27 @@ class WaitingListEntry(LoggedModel):
|
||||
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
|
||||
WaitingListEntry.clean_subevent(self.event, self.subevent)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
if 'name_parts' in update_fields:
|
||||
update_fields.append('name_cached')
|
||||
self.name_cached = self.name
|
||||
if self.name_parts is None:
|
||||
self.name_parts = {}
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if not self.name_parts:
|
||||
return None
|
||||
if '_legacy' in self.name_parts:
|
||||
return self.name_parts['_legacy']
|
||||
if '_scheme' in self.name_parts:
|
||||
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
|
||||
else:
|
||||
scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
|
||||
return scheme['concatenation'](self.name_parts).strip()
|
||||
|
||||
def send_voucher(self, quota_cache=None, user=None, auth=None):
|
||||
availability = (
|
||||
self.variation.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache)
|
||||
|
||||
+13
-10
@@ -970,17 +970,17 @@ class ManualPayment(BasePaymentProvider):
|
||||
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}.'),
|
||||
'the placeholders {order}, {amount}, {currency} and {amount_with_currency}.'),
|
||||
widget=I18nTextarea,
|
||||
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
|
||||
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_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}.'),
|
||||
'the placeholders {order}, {amount}, {currency} and {amount_with_currency}.'),
|
||||
widget=I18nTextarea,
|
||||
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
|
||||
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
|
||||
)),
|
||||
] + list(super().settings_form_fields.items())
|
||||
)
|
||||
@@ -1001,21 +1001,24 @@ class ManualPayment(BasePaymentProvider):
|
||||
def checkout_confirm_render(self, request):
|
||||
return self.payment_form_render(request)
|
||||
|
||||
def format_map(self, order):
|
||||
def format_map(self, order, payment):
|
||||
return {
|
||||
'order': order.code,
|
||||
'total': order.total,
|
||||
'amount': payment.amount,
|
||||
'currency': self.event.currency,
|
||||
'total_with_currency': money_filter(order.total, self.event.currency)
|
||||
'amount_with_currency': money_filter(payment.amount, self.event.currency),
|
||||
# {total} and {total_with_currency} are deprecated
|
||||
'total': order.total,
|
||||
'total_with_currency': money_filter(order.total, self.event.currency),
|
||||
}
|
||||
|
||||
def order_pending_mail_render(self, order) -> str:
|
||||
msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order))
|
||||
def order_pending_mail_render(self, order, payment) -> str:
|
||||
msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order, payment))
|
||||
return msg
|
||||
|
||||
def payment_pending_render(self, request, payment) -> str:
|
||||
return rich_text(
|
||||
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(payment.order))
|
||||
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(payment.order, payment))
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -298,6 +298,11 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"editor_sample": _("Event organizer info text"),
|
||||
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
|
||||
}),
|
||||
("event_info_text", {
|
||||
"label": _("Event info text"),
|
||||
"editor_sample": _("Event info text"),
|
||||
"evaluate": lambda op, order, ev: str(order.event.settings.event_info_text)
|
||||
}),
|
||||
("now_date", {
|
||||
"label": _("Printing date"),
|
||||
"editor_sample": _("2017-05-31"),
|
||||
|
||||
@@ -241,7 +241,7 @@ class CartManager:
|
||||
raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,))
|
||||
|
||||
def _check_item_constraints(self, op, current_ops=[]):
|
||||
if isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
|
||||
if isinstance(op, (self.AddOperation, self.ExtendOperation)):
|
||||
if not (
|
||||
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
|
||||
(isinstance(op, self.ExtendOperation) and op.position.is_bundled)
|
||||
@@ -863,7 +863,7 @@ class CartManager:
|
||||
op.position.addons.all().delete()
|
||||
op.position.delete()
|
||||
|
||||
elif isinstance(op, self.AddOperation) or isinstance(op, self.ExtendOperation):
|
||||
elif isinstance(op, (self.AddOperation, self.ExtendOperation)):
|
||||
# Create a CartPosition for as much items as we can
|
||||
requested_count = quota_available_count = voucher_available_count = op.count
|
||||
|
||||
|
||||
@@ -171,9 +171,17 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
if invoice.event.has_subevents:
|
||||
desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent)
|
||||
InvoiceLine.objects.create(
|
||||
position=i, invoice=invoice, description=desc,
|
||||
gross_value=p.price, tax_value=p.tax_value,
|
||||
subevent=p.subevent, event_date_from=(p.subevent.date_from if p.subevent else invoice.event.date_from),
|
||||
position=i,
|
||||
invoice=invoice,
|
||||
description=desc,
|
||||
gross_value=p.price,
|
||||
tax_value=p.tax_value,
|
||||
subevent=p.subevent,
|
||||
item=p.item,
|
||||
variation=p.variation,
|
||||
attendee_name=p.attendee_name if invoice.event.settings.invoice_attendee_name else None,
|
||||
event_date_from=p.subevent.date_from if invoice.event.has_subevents else invoice.event.date_from,
|
||||
event_date_to=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to,
|
||||
tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
|
||||
)
|
||||
|
||||
@@ -198,6 +206,8 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
invoice=invoice,
|
||||
description=fee_title,
|
||||
gross_value=fee.value,
|
||||
event_date_from=None if invoice.event.has_subevents else invoice.event.date_from,
|
||||
event_date_to=None if invoice.event.has_subevents else invoice.event.date_to,
|
||||
tax_value=fee.tax_value,
|
||||
tax_rate=fee.tax_rate,
|
||||
tax_name=fee.tax_rule.name if fee.tax_rule else ''
|
||||
|
||||
@@ -19,6 +19,7 @@ from django.core.mail import (
|
||||
EmailMultiAlternatives, SafeMIMEMultipart, get_connection,
|
||||
)
|
||||
from django.core.mail.message import SafeMIMEText
|
||||
from django.db import transaction
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import gettext as _, pgettext
|
||||
from django_scopes import scope, scopes_disabled
|
||||
@@ -240,7 +241,15 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
task_chain = []
|
||||
|
||||
task_chain.append(send_task)
|
||||
chain(*task_chain).apply_async()
|
||||
|
||||
if 'locmem' in settings.EMAIL_BACKEND:
|
||||
# This clause is triggered during unit tests, because transaction.on_commit never fires due to the nature
|
||||
# Django's unit tests work
|
||||
chain(*task_chain).apply_async()
|
||||
else:
|
||||
transaction.on_commit(
|
||||
lambda: chain(*task_chain).apply_async()
|
||||
)
|
||||
|
||||
|
||||
class CustomEmail(EmailMultiAlternatives):
|
||||
|
||||
@@ -327,7 +327,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
||||
|
||||
|
||||
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
|
||||
cancellation_fee=None, keep_fees=None):
|
||||
cancellation_fee=None, keep_fees=None, cancel_invoice=True):
|
||||
"""
|
||||
Mark this order as canceled
|
||||
:param order: The order to change
|
||||
@@ -351,9 +351,10 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
if not order.cancel_allowed():
|
||||
raise OrderError(_('You cannot cancel this order.'))
|
||||
invoices = []
|
||||
i = order.invoices.filter(is_cancellation=False).last()
|
||||
if i and not i.refered.exists():
|
||||
invoices.append(generate_cancellation(i))
|
||||
if cancel_invoice:
|
||||
i = order.invoices.filter(is_cancellation=False).last()
|
||||
if i and not i.refered.exists():
|
||||
invoices.append(generate_cancellation(i))
|
||||
|
||||
for position in order.positions.all():
|
||||
for gc in position.issued_gift_cards.all():
|
||||
@@ -403,7 +404,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
order.cancellation_date = now()
|
||||
order.save(update_fields=['status', 'cancellation_date', 'total'])
|
||||
|
||||
if i:
|
||||
if cancel_invoice and i:
|
||||
invoices.append(generate_invoice(order))
|
||||
else:
|
||||
with order.event.lock():
|
||||
@@ -2152,11 +2153,12 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
@scopes_disabled()
|
||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
|
||||
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None):
|
||||
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False, comment=None,
|
||||
cancel_invoice=True):
|
||||
try:
|
||||
try:
|
||||
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
|
||||
cancellation_fee)
|
||||
cancellation_fee, cancel_invoice=cancel_invoice)
|
||||
if try_auto_refund:
|
||||
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard,
|
||||
comment=comment)
|
||||
|
||||
@@ -3,22 +3,21 @@ from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Event, User, Voucher
|
||||
from pretix.base.models import Event, LogEntry, User, Voucher
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.services.tasks import TransactionAwareProfiledEventTask
|
||||
from pretix.celery_app import app
|
||||
|
||||
|
||||
@app.task(base=TransactionAwareProfiledEventTask, acks_late=True)
|
||||
def vouchers_send(event: Event, vouchers: list, subject: str, message: str, recipients: list, user: int) -> None:
|
||||
def vouchers_send(event: Event, vouchers: list, subject: str, message: str, recipients: list, user: int,
|
||||
progress=None) -> None:
|
||||
vouchers = list(Voucher.objects.filter(id__in=vouchers).order_by('id'))
|
||||
user = User.objects.get(pk=user)
|
||||
for r in recipients:
|
||||
for ir, r in enumerate(recipients):
|
||||
voucher_list = []
|
||||
for i in range(r['number']):
|
||||
voucher_list.append(vouchers.pop())
|
||||
with language(event.settings.locale):
|
||||
email_context = get_email_context(event=event, name=r.get('name') or '', voucher_list=[v.code for v in voucher_list])
|
||||
email_context = get_email_context(event=event, name=r.get('name') or '',
|
||||
voucher_list=[v.code for v in voucher_list])
|
||||
mail(
|
||||
r['email'],
|
||||
subject,
|
||||
@@ -27,14 +26,14 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
||||
event,
|
||||
locale=event.settings.locale,
|
||||
)
|
||||
logs = []
|
||||
for v in voucher_list:
|
||||
if r.get('tag') and r.get('tag') != v.tag:
|
||||
v.tag = r.get('tag')
|
||||
if v.comment:
|
||||
v.comment += '\n\n'
|
||||
v.comment = gettext('The voucher has been sent to {recipient}.').format(recipient=r['email'])
|
||||
v.save(update_fields=['tag', 'comment'])
|
||||
v.log_action(
|
||||
logs.append(v.log_action(
|
||||
'pretix.voucher.sent',
|
||||
user=user,
|
||||
data={
|
||||
@@ -42,5 +41,11 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
||||
'name': r.get('name'),
|
||||
'subject': subject,
|
||||
'message': message,
|
||||
}
|
||||
)
|
||||
},
|
||||
save=False
|
||||
))
|
||||
Voucher.objects.bulk_update(voucher_list, fields=['comment', 'tag'], batch_size=500)
|
||||
LogEntry.objects.bulk_create(logs, batch_size=500)
|
||||
|
||||
if progress and ir % 50 == 0:
|
||||
progress(ir / len(recipients))
|
||||
|
||||
@@ -975,6 +975,61 @@ DEFAULTS = {
|
||||
widget=forms.NumberInput(),
|
||||
)
|
||||
},
|
||||
'waiting_list_names_asked': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for a name"),
|
||||
help_text=_("Ask for a name when signing up to the waiting list."),
|
||||
)
|
||||
},
|
||||
'waiting_list_names_required': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Require name"),
|
||||
help_text=_("Require a name when signing up to the waiting list.."),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-waiting_list_names_asked'}),
|
||||
)
|
||||
},
|
||||
'waiting_list_phones_asked': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for a phone number"),
|
||||
help_text=_("Ask for a phone number when signing up to the waiting list."),
|
||||
)
|
||||
},
|
||||
'waiting_list_phones_required': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Require phone number"),
|
||||
help_text=_("Require a phone number when signing up to the waiting list.."),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-waiting_list_phones_asked'}),
|
||||
)
|
||||
},
|
||||
'waiting_list_phones_explanation_text': {
|
||||
'default': '',
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nFormField,
|
||||
'serializer_class': I18nField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Phone number explanation"),
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.")
|
||||
)
|
||||
},
|
||||
|
||||
'ticket_download': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -1744,7 +1799,7 @@ Your {event} team"""))
|
||||
),
|
||||
},
|
||||
'theme_color_danger': {
|
||||
'default': '#D36060',
|
||||
'default': '#C44F4F',
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
@@ -1945,6 +2000,18 @@ Your {event} team"""))
|
||||
widget=I18nTextarea
|
||||
)
|
||||
},
|
||||
'event_info_text': {
|
||||
'default': '',
|
||||
'type': LazyI18nString,
|
||||
'serializer_class': I18nField,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Info text'),
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
help_text=_('Not displayed anywhere by default, but if you want to, you can use this e.g. in ticket templates.')
|
||||
)
|
||||
},
|
||||
'banner_text': {
|
||||
'default': '',
|
||||
'type': LazyI18nString,
|
||||
|
||||
@@ -203,7 +203,7 @@ class EmailAddressShredder(BaseDataShredder):
|
||||
class WaitingListShredder(BaseDataShredder):
|
||||
verbose_name = _('Waiting list')
|
||||
identifier = 'waiting_list'
|
||||
description = _('This will remove all email addresses from the waiting list.')
|
||||
description = _('This will remove all names, email addresses, and phone numbers from the waiting list.')
|
||||
|
||||
def generate_files(self) -> List[Tuple[str, str, str]]:
|
||||
yield 'waiting-list.json', 'application/json', json.dumps([
|
||||
@@ -213,7 +213,7 @@ class WaitingListShredder(BaseDataShredder):
|
||||
|
||||
@transaction.atomic
|
||||
def shred_data(self):
|
||||
self.event.waitinglistentries.update(email='█')
|
||||
self.event.waitinglistentries.update(name_cached=None, name_parts={'_shredded': True}, email='█', phone='█')
|
||||
|
||||
for wle in self.event.waitinglistentries.select_related('voucher').filter(voucher__isnull=False):
|
||||
if '@' in wle.voucher.comment:
|
||||
@@ -222,7 +222,14 @@ class WaitingListShredder(BaseDataShredder):
|
||||
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.voucher.added.waitinglist").exclude(data=""):
|
||||
d = le.parsed_data
|
||||
if 'name' in d:
|
||||
d['name'] = '█'
|
||||
if 'name_parts' in d:
|
||||
d['name_parts'] = {
|
||||
'_legacy': '█'
|
||||
}
|
||||
d['email'] = '█'
|
||||
d['phone'] = '█'
|
||||
le.data = json.dumps(d)
|
||||
le.shredded = True
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
|
||||
@@ -136,6 +136,31 @@
|
||||
text-decoration: none;
|
||||
color: {{ color }};
|
||||
}
|
||||
|
||||
.order-button {
|
||||
padding-top: 5px
|
||||
}
|
||||
.order-button a.button {
|
||||
font-size: 12px;
|
||||
}
|
||||
.order-info {
|
||||
padding-bottom: 5px
|
||||
}
|
||||
|
||||
.order {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cart-table > tr > td:first-child {
|
||||
width: 40px;
|
||||
}
|
||||
.order-details > tr > td:first-child {
|
||||
width: 20%;
|
||||
}
|
||||
.order-details td {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
{% if rtl %}
|
||||
body {
|
||||
direction: rtl;
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
{% load eventurl %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if position %}
|
||||
<div class="order-info">
|
||||
{% trans "You are receiving this email because someone signed you up for the following event:" %}
|
||||
</div>
|
||||
<table class="order-details">
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{% trans "Event:" %}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ event.name }}
|
||||
<br>
|
||||
{% if event.has_subevents and ev.name|upper != event.name|upper %}{{ ev.name }}<br>{% endif %}
|
||||
{{ ev.get_date_range_display }}
|
||||
{% if event.settings.show_times %}
|
||||
{{ ev.date_from|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{% trans "Order code:" %}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ order.code }} ({{ order.datetime|date:"SHORT_DATE_FORMAT" }})<br>
|
||||
{% if order.email %}
|
||||
{% trans "created by" %} {{ order.email }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{% trans "Organizer:" %}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ event.organizer }}
|
||||
{% if event.settings.contact_mail %}
|
||||
<br>
|
||||
<a href="mailto:{{ event.settings.contact_mail }}">
|
||||
{{ event.settings.contact_mail }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="order-button">
|
||||
<a href="{% abseventurl event "presale:event.order.position" order=order.code secret=position.web_secret position=position.positionid %}" class="button">
|
||||
{% trans "View registration details" %}
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="order-info">
|
||||
{% trans "You are receiving this email because you placed an order for the following event:" %}
|
||||
</div>
|
||||
<table class="order-details">
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{% trans "Event:" %}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ event.name }}
|
||||
{% if not event.has_subevents and event.settings.show_dates_on_frontpage %}
|
||||
<br>
|
||||
{{ event.get_date_range_display }}
|
||||
{% if event.settings.show_times %}
|
||||
{{ event.date_from|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{% trans "Order code:" %}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ order.code }} ({{ order.datetime|date:"SHORT_DATE_FORMAT" }})
|
||||
</td>
|
||||
</tr>
|
||||
{% if cart %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{% trans "Details:" %}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<table class="cart-table">
|
||||
{% for groupkey, positions in cart %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if not groupkey.4 %} {# is addon #}
|
||||
{{ positions|length }}x
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if groupkey.4 %} {# is addon #}
|
||||
+
|
||||
{% endif %}
|
||||
{{ groupkey.0.name }}{% if groupkey.1 %} – {{ groupkey.1.value }}{% endif %}
|
||||
{% if groupkey.2 %} {# subevent #}
|
||||
<br>
|
||||
{% if groupkey.2.name|upper != event.name|upper %}
|
||||
{{ groupkey.2.name }} ·
|
||||
{% endif %}
|
||||
{{ groupkey.2.get_date_range_display }}
|
||||
{% if event.settings.show_times %}
|
||||
{{ groupkey.2.date_from|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if groupkey.3 %} {# attendee name #}
|
||||
<br>
|
||||
{{ groupkey.3.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{% trans "Organizer:" %}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ event.organizer }}
|
||||
{% if event.settings.contact_mail %}
|
||||
<br>
|
||||
<a href="mailto:{{ event.settings.contact_mail }}">
|
||||
{{ event.settings.contact_mail }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="order-button">
|
||||
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}" class="button">
|
||||
{% trans "View order details" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -23,23 +23,7 @@
|
||||
<table cellpadding="20"><tr><td>
|
||||
<![endif]-->
|
||||
<div class="content">
|
||||
{% if position %}
|
||||
{% trans "You are receiving this email because someone signed you up for the following event:" %}<br>
|
||||
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
|
||||
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
|
||||
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
|
||||
<a href="{% abseventurl event "presale:event.order.position" order=order.code secret=position.web_secret position=position.positionid %}">
|
||||
{% trans "View registration details" %}
|
||||
</a>
|
||||
{% else %}
|
||||
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
|
||||
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
|
||||
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
|
||||
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
|
||||
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}">
|
||||
{% trans "View order details" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% include "pretixbase/email/order_details.html" %}
|
||||
</div>
|
||||
<!--[if gte mso 9]>
|
||||
</td></tr></table>
|
||||
|
||||
@@ -147,6 +147,31 @@
|
||||
text-decoration: none;
|
||||
color: {{ color }};
|
||||
}
|
||||
|
||||
.order-button {
|
||||
padding-top: 5px
|
||||
}
|
||||
.order-button a.button {
|
||||
font-size: 12px;
|
||||
}
|
||||
.order-info {
|
||||
padding-bottom: 5px
|
||||
}
|
||||
|
||||
.order {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cart-table > tr > td:first-child {
|
||||
width: 40px;
|
||||
}
|
||||
.order-details > tr > td:first-child {
|
||||
width: 20%;
|
||||
}
|
||||
.order-details td {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
{% if rtl %}
|
||||
body {
|
||||
direction: rtl;
|
||||
@@ -226,23 +251,7 @@
|
||||
<table cellpadding="20"><tr><td>
|
||||
<![endif]-->
|
||||
<div class="content">
|
||||
{% if position %}
|
||||
{% trans "You are receiving this email because someone signed you up for the following event:" %}<br>
|
||||
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
|
||||
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
|
||||
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
|
||||
<a href="{% abseventurl event "presale:event.order.position" order=order.code secret=position.web_secret position=position.positionid %}">
|
||||
{% trans "View registration details" %}
|
||||
</a>
|
||||
{% else %}
|
||||
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
|
||||
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
|
||||
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
|
||||
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
|
||||
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}">
|
||||
{% trans "View order details" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% include "pretixbase/email/order_details.html" %}
|
||||
</div>
|
||||
<!--[if gte mso 9]>
|
||||
</td></tr></table>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% load thumb %}
|
||||
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
|
||||
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}">
|
||||
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.value.name }}"><img src="{{ widget.value|thumb:"200x100" }}" /></a>{% endif %}<br>
|
||||
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}{% if widget.value.is_img %}<br><a href="{{ widget.value.url }}" data-lightbox="{{ widget.name }}"><img src="{{ widget.value|thumb:"200x100" }}" /></a>{% endif %}<br>
|
||||
{{ widget.input_text }}:{% endif %}
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
|
||||
{% if widget.cachedfile %}<input type="hidden" name="{{ widget.hidden_name }}" value="{{ widget.cachedfile.id }}">{% endif %}
|
||||
|
||||
@@ -11,7 +11,7 @@ register = template.Library()
|
||||
|
||||
@register.filter("money")
|
||||
def money_filter(value: Decimal, arg='', hide_currency=False):
|
||||
if isinstance(value, float) or isinstance(value, int):
|
||||
if isinstance(value, (float, int)):
|
||||
value = Decimal(value)
|
||||
if not isinstance(value, Decimal):
|
||||
if value == '':
|
||||
@@ -47,7 +47,7 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
|
||||
|
||||
@register.filter("money_numberfield")
|
||||
def money_numberfield_filter(value: Decimal, arg=''):
|
||||
if isinstance(value, float) or isinstance(value, int):
|
||||
if isinstance(value, (float, int)):
|
||||
value = Decimal(value)
|
||||
if not isinstance(value, Decimal):
|
||||
raise TypeError("Invalid data type passed to money filter: %r" % type(value))
|
||||
|
||||
@@ -48,7 +48,7 @@ def page_not_found(request, exception):
|
||||
except (AttributeError, IndexError):
|
||||
pass
|
||||
else:
|
||||
if isinstance(message, str) or isinstance(message, Promise):
|
||||
if isinstance(message, (str, Promise)):
|
||||
exception_repr = str(message)
|
||||
context = {
|
||||
'request_path': request.path,
|
||||
|
||||
+129
-31
@@ -4,43 +4,25 @@ import celery.exceptions
|
||||
from celery.result import AsyncResult
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.test import RequestFactory
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import FormView
|
||||
|
||||
from pretix.base.models import User
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.celery_app import app
|
||||
|
||||
logger = logging.getLogger('pretix.base.tasks')
|
||||
|
||||
|
||||
class AsyncAction:
|
||||
task = None
|
||||
class AsyncMixin:
|
||||
success_url = None
|
||||
error_url = None
|
||||
known_errortypes = []
|
||||
|
||||
def do(self, *args, **kwargs):
|
||||
if not isinstance(self.task, app.Task):
|
||||
raise TypeError('Method has no task attached')
|
||||
|
||||
try:
|
||||
res = self.task.apply_async(args=args, kwargs=kwargs)
|
||||
except ConnectionError:
|
||||
# Task very likely not yet sent, due to redis restarting etc. Let's try once agan
|
||||
res = self.task.apply_async(args=args, kwargs=kwargs)
|
||||
|
||||
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
|
||||
data = self._return_ajax_result(res)
|
||||
data['check_url'] = self.get_check_url(res.id, True)
|
||||
return JsonResponse(data)
|
||||
else:
|
||||
if res.ready():
|
||||
if res.successful() and not isinstance(res.info, Exception):
|
||||
return self.success(res.info)
|
||||
else:
|
||||
return self.error(res.info)
|
||||
return redirect(self.get_check_url(res.id, False))
|
||||
|
||||
def get_success_url(self, value):
|
||||
return self.success_url
|
||||
|
||||
@@ -50,11 +32,6 @@ class AsyncAction:
|
||||
def get_check_url(self, task_id, ajax):
|
||||
return self.request.path + '?async_id=%s' % task_id + ('&ajax=1' if ajax else '')
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
return self.get_result(request)
|
||||
return self.http_method_not_allowed(request)
|
||||
|
||||
def _ajax_response_data(self):
|
||||
return {}
|
||||
|
||||
@@ -86,7 +63,7 @@ class AsyncAction:
|
||||
if smes:
|
||||
messages.success(self.request, smes)
|
||||
# TODO: Do not store message if the ajax client states that it will not redirect
|
||||
# but handle the mssage itself
|
||||
# but handle the message itself
|
||||
data.update({
|
||||
'redirect': self.get_success_url(res.info),
|
||||
'success': True,
|
||||
@@ -95,7 +72,7 @@ class AsyncAction:
|
||||
else:
|
||||
messages.error(self.request, self.get_error_message(res.info))
|
||||
# TODO: Do not store message if the ajax client states that it will not redirect
|
||||
# but handle the mssage itself
|
||||
# but handle the message itself
|
||||
data.update({
|
||||
'redirect': self.get_error_url(),
|
||||
'success': False,
|
||||
@@ -159,3 +136,124 @@ class AsyncAction:
|
||||
|
||||
def get_success_message(self, value):
|
||||
return _('The task has been completed.')
|
||||
|
||||
|
||||
class AsyncAction(AsyncMixin):
|
||||
task = None
|
||||
|
||||
def do(self, *args, **kwargs):
|
||||
if not isinstance(self.task, app.Task):
|
||||
raise TypeError('Method has no task attached')
|
||||
|
||||
try:
|
||||
res = self.task.apply_async(args=args, kwargs=kwargs)
|
||||
except ConnectionError:
|
||||
# Task very likely not yet sent, due to redis restarting etc. Let's try once again
|
||||
res = self.task.apply_async(args=args, kwargs=kwargs)
|
||||
|
||||
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
|
||||
data = self._return_ajax_result(res)
|
||||
data['check_url'] = self.get_check_url(res.id, True)
|
||||
return JsonResponse(data)
|
||||
else:
|
||||
if res.ready():
|
||||
if res.successful() and not isinstance(res.info, Exception):
|
||||
return self.success(res.info)
|
||||
else:
|
||||
return self.error(res.info)
|
||||
return redirect(self.get_check_url(res.id, False))
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
return self.get_result(request)
|
||||
return self.http_method_not_allowed(request)
|
||||
|
||||
|
||||
class AsyncFormView(AsyncMixin, FormView):
|
||||
"""
|
||||
FormView variant in which instead of ``form_valid``, an ``async_form_valid``
|
||||
is executed in a celery task. Note that this places some severe limitations
|
||||
on the form and the view, e.g. neither ``get_form*`` nor the form itself
|
||||
may depend on the request object unless specifically supported by this class.
|
||||
Also, all form keyword arguments except ``instance`` need to be serializable.
|
||||
"""
|
||||
known_errortypes = ['ValidationError']
|
||||
|
||||
def __init_subclass__(cls):
|
||||
def async_execute(self, request_path, form_kwargs, organizer=None, event=None, user=None):
|
||||
view_instance = cls()
|
||||
view_instance.request = RequestFactory().post(request_path)
|
||||
if organizer:
|
||||
view_instance.request.event = event
|
||||
if organizer:
|
||||
view_instance.request.organizer = organizer
|
||||
if user:
|
||||
view_instance.request.user = User.objects.get(pk=user)
|
||||
|
||||
form_class = view_instance.get_form_class()
|
||||
if form_kwargs.get('instance'):
|
||||
cls.model.objects.get(pk=form_kwargs['instance'])
|
||||
|
||||
form_kwargs = view_instance.get_async_form_kwargs(form_kwargs, organizer, event)
|
||||
|
||||
form = form_class(**form_kwargs)
|
||||
return view_instance.async_form_valid(self, form)
|
||||
|
||||
cls.async_execute = app.task(
|
||||
base=ProfiledEventTask,
|
||||
bind=True,
|
||||
name=cls.__module__ + '.' + cls.__name__ + '.async_execute',
|
||||
throws=(ValidationError,)
|
||||
)(async_execute)
|
||||
|
||||
def async_form_valid(self, task, form):
|
||||
pass
|
||||
|
||||
def get_async_form_kwargs(self, form_kwargs, organizer=None, event=None):
|
||||
return form_kwargs
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
return self.get_result(request)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.files:
|
||||
raise TypeError('File upload currently not supported in AsyncFormView')
|
||||
form_kwargs = {
|
||||
k: v for k, v in self.get_form_kwargs().items()
|
||||
}
|
||||
if form_kwargs.get('instance'):
|
||||
if form_kwargs['instance'].pk:
|
||||
form_kwargs['instance'] = form_kwargs['instance'].pk
|
||||
else:
|
||||
form_kwargs['instance'] = None
|
||||
form_kwargs.setdefault('data', {})
|
||||
kwargs = {
|
||||
'request_path': self.request.path,
|
||||
'form_kwargs': form_kwargs,
|
||||
}
|
||||
if hasattr(self.request, 'organizer'):
|
||||
kwargs['organizer'] = self.request.organizer.pk
|
||||
if self.request.user.is_authenticated:
|
||||
kwargs['user'] = self.request.user.pk
|
||||
if hasattr(self.request, 'event'):
|
||||
kwargs['event'] = self.request.event.pk
|
||||
|
||||
try:
|
||||
res = type(self).async_execute.apply_async(kwargs=kwargs)
|
||||
except ConnectionError:
|
||||
# Task very likely not yet sent, due to redis restarting etc. Let's try once again
|
||||
res = type(self).async_execute.apply_async(kwargs=kwargs)
|
||||
|
||||
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
|
||||
data = self._return_ajax_result(res)
|
||||
data['check_url'] = self.get_check_url(res.id, True)
|
||||
return JsonResponse(data)
|
||||
else:
|
||||
if res.ready():
|
||||
if res.successful() and not isinstance(res.info, Exception):
|
||||
return self.success(res.info)
|
||||
else:
|
||||
return self.error(res.info)
|
||||
return redirect(self.get_check_url(res.id, False))
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.files import File
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.forms.utils import from_current_timezone
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -122,7 +123,7 @@ class CachedFileInput(forms.ClearableFileInput):
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.file.file.url
|
||||
return reverse('cachedfile.download', kwargs={'id': self.file.id})
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
from ...base.models import CachedFile
|
||||
@@ -200,6 +201,8 @@ class CachedFileField(ExtFileField):
|
||||
from ...base.models import CachedFile
|
||||
|
||||
if isinstance(data, File):
|
||||
if hasattr(data, '_uploaded_to'):
|
||||
return data._uploaded_to
|
||||
cf = CachedFile.objects.create(
|
||||
expires=now() + datetime.timedelta(days=1),
|
||||
date=now(),
|
||||
@@ -209,6 +212,7 @@ class CachedFileField(ExtFileField):
|
||||
)
|
||||
cf.file.save(data.name, data.file)
|
||||
cf.save()
|
||||
data._uploaded_to = cf
|
||||
return cf
|
||||
return super().bound_data(data, initial)
|
||||
|
||||
@@ -217,6 +221,8 @@ class CachedFileField(ExtFileField):
|
||||
|
||||
data = super().clean(*args, **kwargs)
|
||||
if isinstance(data, File):
|
||||
if hasattr(data, '_uploaded_to'):
|
||||
return data._uploaded_to
|
||||
cf = CachedFile.objects.create(
|
||||
expires=now() + datetime.timedelta(days=1),
|
||||
web_download=True,
|
||||
@@ -226,6 +232,7 @@ class CachedFileField(ExtFileField):
|
||||
)
|
||||
cf.file.save(data.name, data.file)
|
||||
cf.save()
|
||||
data._uploaded_to = cf
|
||||
return cf
|
||||
return data
|
||||
|
||||
|
||||
@@ -449,6 +449,11 @@ class EventSettingsForm(SettingsForm):
|
||||
'waiting_list_enabled',
|
||||
'waiting_list_hours',
|
||||
'waiting_list_auto',
|
||||
'waiting_list_names_asked',
|
||||
'waiting_list_names_required',
|
||||
'waiting_list_phones_asked',
|
||||
'waiting_list_phones_required',
|
||||
'waiting_list_phones_explanation_text',
|
||||
'max_items_per_order',
|
||||
'reservation_time',
|
||||
'contact_mail',
|
||||
@@ -459,6 +464,7 @@ class EventSettingsForm(SettingsForm):
|
||||
'frontpage_subevent_ordering',
|
||||
'event_list_type',
|
||||
'frontpage_text',
|
||||
'event_info_text',
|
||||
'attendee_names_asked',
|
||||
'attendee_names_required',
|
||||
'attendee_emails_asked',
|
||||
|
||||
@@ -5,7 +5,9 @@ from urllib.parse import urlencode
|
||||
from django import forms
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db.models import Exists, F, Max, Model, OuterRef, Q, QuerySet
|
||||
from django.db.models import (
|
||||
Count, Exists, F, Max, Model, OuterRef, Q, QuerySet,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, ExtractWeekDay
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.formats import date_format, localize
|
||||
@@ -153,6 +155,7 @@ class OrderFilterForm(FilterForm):
|
||||
(Order.STATUS_CANCELED, _('Canceled (fully)')),
|
||||
('cp', _('Canceled (fully or with paid fee)')),
|
||||
('rc', _('Cancellation requested')),
|
||||
('cni', _('Fully canceled but invoice not canceled')),
|
||||
)),
|
||||
(_('Payment process'), (
|
||||
(Order.STATUS_EXPIRED, _('Expired')),
|
||||
@@ -264,6 +267,18 @@ class OrderFilterForm(FilterForm):
|
||||
status=Order.STATUS_PAID,
|
||||
pending_sum_t__gt=0
|
||||
)
|
||||
elif s == 'cni':
|
||||
i = Invoice.objects.filter(
|
||||
order=OuterRef('pk'),
|
||||
is_cancellation=False,
|
||||
refered__isnull=True,
|
||||
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||
qs = qs.annotate(
|
||||
icnt=i
|
||||
).filter(
|
||||
icnt__gt=0,
|
||||
status=Order.STATUS_CANCELED,
|
||||
)
|
||||
elif s == 'pa':
|
||||
qs = qs.filter(
|
||||
status=Order.STATUS_PENDING,
|
||||
@@ -1424,6 +1439,8 @@ class VoucherFilterForm(FilterForm):
|
||||
s = fdata.get('tag').strip()
|
||||
if s == '<>':
|
||||
qs = qs.filter(Q(tag__isnull=True) | Q(tag=''))
|
||||
elif s[0] == '"' and s[-1] == '"':
|
||||
qs = qs.filter(tag__iexact=s[1:-1])
|
||||
else:
|
||||
qs = qs.filter(tag__icontains=s)
|
||||
|
||||
|
||||
@@ -337,7 +337,7 @@ class ItemCreateForm(I18nModelForm):
|
||||
setattr(self.instance, f, getattr(self.cleaned_data['copy_from'], f))
|
||||
else:
|
||||
# Add to all sales channels by default
|
||||
self.instance.sales_channels = [k for k in get_all_sales_channels().keys()]
|
||||
self.instance.sales_channels = list(get_all_sales_channels().keys())
|
||||
|
||||
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
@@ -75,7 +75,7 @@ class ExtendForm(I18nModelForm):
|
||||
return super().save(commit)
|
||||
|
||||
|
||||
class ConfirmPaymentForm(forms.Form):
|
||||
class ForceQuotaConfirmationForm(forms.Form):
|
||||
force = forms.BooleanField(
|
||||
label=_('Overbook quota and ignore late payment'),
|
||||
help_text=_('If you check this box, this operation will be performed even if it leads to an overbooked quota '
|
||||
@@ -101,7 +101,15 @@ class ConfirmPaymentForm(forms.Form):
|
||||
del self.fields['force']
|
||||
|
||||
|
||||
class CancelForm(ConfirmPaymentForm):
|
||||
class ConfirmPaymentForm(ForceQuotaConfirmationForm):
|
||||
pass
|
||||
|
||||
|
||||
class ReactivateOrderForm(ForceQuotaConfirmationForm):
|
||||
pass
|
||||
|
||||
|
||||
class CancelForm(ForceQuotaConfirmationForm):
|
||||
send_email = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Notify customer by email'),
|
||||
@@ -117,6 +125,11 @@ class CancelForm(ConfirmPaymentForm):
|
||||
'in your cancellation fee if you want to keep them. Please always enter a gross value, '
|
||||
'tax will be calculated automatically.'),
|
||||
)
|
||||
cancel_invoice = forms.BooleanField(
|
||||
label=_('Generate cancellation for invoice'),
|
||||
initial=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -130,6 +143,8 @@ class CancelForm(ConfirmPaymentForm):
|
||||
self.fields['cancellation_fee'].max_value = prs
|
||||
else:
|
||||
del self.fields['cancellation_fee']
|
||||
if not self.instance.invoices.exists():
|
||||
del self.fields['cancel_invoice']
|
||||
|
||||
def clean_cancellation_fee(self):
|
||||
val = self.cleaned_data['cancellation_fee'] or Decimal('0.00')
|
||||
|
||||
@@ -5,7 +5,7 @@ from io import StringIO
|
||||
from django import forms
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import EmailValidator
|
||||
from django.db.models.functions import Lower
|
||||
from django.db.models.functions import Upper
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes.forms import SafeModelChoiceField
|
||||
@@ -346,8 +346,8 @@ class VoucherBulkForm(VoucherForm):
|
||||
data = super().clean()
|
||||
|
||||
vouchers = self.instance.event.vouchers.annotate(
|
||||
code_lower=Lower('code')
|
||||
).filter(code_lower__in=[c.lower() for c in data['codes']])
|
||||
code_upper=Upper('code')
|
||||
).filter(code_upper__in=[c.upper() for c in data['codes']])
|
||||
if vouchers.exists():
|
||||
raise ValidationError(_('A voucher with one of these codes already exists.'))
|
||||
|
||||
@@ -377,26 +377,5 @@ class VoucherBulkForm(VoucherForm):
|
||||
|
||||
return data
|
||||
|
||||
def save(self, event, *args, **kwargs):
|
||||
objs = []
|
||||
for code in self.cleaned_data['codes']:
|
||||
obj = modelcopy(self.instance)
|
||||
obj.event = event
|
||||
obj.code = code
|
||||
try:
|
||||
obj.seat = self.cleaned_data['seats'].pop()
|
||||
obj.item = obj.seat.product
|
||||
except IndexError:
|
||||
pass
|
||||
data = dict(self.cleaned_data)
|
||||
data['code'] = code
|
||||
data['bulk'] = True
|
||||
del data['codes']
|
||||
objs.append(obj)
|
||||
Voucher.objects.bulk_create(objs, batch_size=200)
|
||||
objs = []
|
||||
for v in event.vouchers.filter(code__in=self.cleaned_data['codes']):
|
||||
# We need to query them again as bulk_create does not fill in .pk values on databases
|
||||
# other than PostgreSQL
|
||||
objs.append(v)
|
||||
return objs
|
||||
def post_bulk_save(self, objs):
|
||||
pass
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
def current_url(request):
|
||||
if len(request.GET):
|
||||
if request.GET:
|
||||
return request.path + '?' + request.GET.urlencode()
|
||||
else:
|
||||
return request.path
|
||||
|
||||
@@ -154,6 +154,11 @@ This signal allows you to replace the form class that is used for modifying vouc
|
||||
You will receive the default form class (or the class set by a previous plugin) in the
|
||||
``cls`` argument so that you can inherit from it.
|
||||
|
||||
Note that this is also called for the voucher bulk creation form, which is executed in
|
||||
an asynchronous context. For the bulk creation form, ``save()`` is not called. Instead,
|
||||
you can implement ``post_bulk_save(saved_vouchers)`` which may be called multiple times
|
||||
for every batch persisted to the database.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
{% if request.event.has_subevents %}
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" with auto_submit=True %}
|
||||
</form>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
<p>
|
||||
<select name="subevent" class="form-control simple-subevent-choice" data-model-select2="event"
|
||||
<select name="subevent" class="form-control{% if auto_submit %} simple-subevent-choice{% endif %}" data-model-select2="event"
|
||||
data-select2-url="{% url "control:event.subevents.select2" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
data-placeholder="{% trans "All dates" context "subevent" %}">
|
||||
{% for se in selected_subevents %}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
|
||||
{% if request.event.has_subevents %}
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" with auto_submit=True %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not request.event.has_subevents or subevent %}
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
{% bootstrap_field sform.checkout_phone_helptext layout="control" %}
|
||||
{% bootstrap_field sform.banner_text layout="control" %}
|
||||
{% bootstrap_field sform.banner_text_bottom layout="control" %}
|
||||
{% bootstrap_field sform.event_info_text layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Shop design" %}</legend>
|
||||
@@ -248,6 +249,9 @@
|
||||
{% bootstrap_field sform.waiting_list_enabled layout="control" %}
|
||||
{% bootstrap_field sform.waiting_list_auto layout="control" %}
|
||||
{% bootstrap_field sform.waiting_list_hours layout="control" %}
|
||||
{% bootstrap_field sform.waiting_list_names_asked_required layout="control" %}
|
||||
{% bootstrap_field sform.waiting_list_phones_asked_required layout="control" %}
|
||||
{% bootstrap_field sform.waiting_list_phones_explanation_text layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Item metadata" %}</legend>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</p>
|
||||
{% if request.event.has_subevents %}
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" with auto_submit=True %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if quotas|length == 0 %}
|
||||
|
||||
@@ -23,13 +23,16 @@
|
||||
<input type="hidden" name="status" value="c"/>
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.send_email layout='' %}
|
||||
{% if form.cancel_invoice %}
|
||||
{% bootstrap_field form.cancel_invoice layout='' %}
|
||||
{% endif %}
|
||||
{% if form.cancellation_fee %}
|
||||
{% bootstrap_field form.cancellation_fee layout='' %}
|
||||
{% endif %}
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% trans "No, take me back" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
<form method="post" class="form-horizontal" href="">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
|
||||
<div class="form-group submit-group">
|
||||
<a class="btn btn-default btn-lg"
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
|
||||
@@ -170,6 +170,10 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
{{ o.total|money:request.event.currency }}
|
||||
{% if o.status == "c" and o.icnt %}
|
||||
<br>
|
||||
<span class="label label-warning">{% trans "INVOICE NOT CANCELED" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">{{ o.pcnt|default_if_none:"0" }}</td>
|
||||
<td class="text-right flip">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block title %}{% trans "Voucher" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Create multiple vouchers" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
<form action="" method="post" class="form-horizontal" data-asynctask>
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<td>
|
||||
<strong>
|
||||
{% if t.tag %}
|
||||
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ t.tag|urlencode }}">
|
||||
<a href="{% url "control:event.vouchers" organizer=request.event.organizer.slug event=request.event.slug %}?tag={{ '"'|add:t.tag|add:'"'|urlencode }}">
|
||||
{{ t.tag }}
|
||||
</a>
|
||||
{% else %}
|
||||
|
||||
@@ -48,15 +48,9 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if request.event.has_subevents %}
|
||||
<select name="subevent" class="form-control">
|
||||
<option value="">{% trans "All dates" context "subevent" %}</option>
|
||||
{% for se in request.event.subevents.all %}
|
||||
<option value="{{ se.id }}"
|
||||
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
|
||||
{{ se.name }} – {{ se.get_date_range_display }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="col-md-6">
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<button class="btn btn-large btn-primary" type="submit">
|
||||
{% trans "Send as many vouchers as possible" %}
|
||||
@@ -80,59 +74,63 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
<select name="status" class="form-control">
|
||||
<option value="a"
|
||||
{% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "All entries" %}</option>
|
||||
<option value="w"
|
||||
{% if request.GET.status == "w" or not request.GET.status %}selected="selected"{% endif %}>
|
||||
{% trans "Waiting for a voucher" %}</option>
|
||||
<option value="s"
|
||||
{% if request.GET.status == "s" %}selected="selected"{% endif %}>{% trans "Voucher assigned" %}</option>
|
||||
<option value="v"
|
||||
{% if request.GET.status == "v" %}selected="selected"{% endif %}>
|
||||
{% trans "Waiting for redemption" %}</option>
|
||||
<option value="r"
|
||||
{% if request.GET.status == "r" %}selected="selected"{% endif %}>
|
||||
{% trans "Successfully redeemed" %}</option>
|
||||
<option value="e"
|
||||
{% if request.GET.status == "e" %}selected="selected"{% endif %}>
|
||||
{% trans "Voucher expired" %}</option>
|
||||
</select>
|
||||
<select name="item" class="form-control">
|
||||
<option value="">{% trans "All products" %}</option>
|
||||
{% for item in items %}
|
||||
<option value="{{ item.id }}"
|
||||
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
|
||||
{{ item }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if request.event.has_subevents %}
|
||||
<select name="subevent" class="form-control">
|
||||
<option value="">{% trans "All dates" context "subevent" %}</option>
|
||||
{% for se in request.event.subevents.all %}
|
||||
<option value="{{ se.id }}"
|
||||
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
|
||||
{{ se.name }} – {{ se.get_date_range_display }}
|
||||
<form class="row filter-form" action="" method="get">
|
||||
<div class="col-lg-2 col-md-3 col-xs-6">
|
||||
<select name="status" class="form-control">
|
||||
<option value="a"
|
||||
{% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "All entries" %}</option>
|
||||
<option value="w"
|
||||
{% if request.GET.status == "w" or not request.GET.status %}selected="selected"{% endif %}>
|
||||
{% trans "Waiting for a voucher" %}</option>
|
||||
<option value="s"
|
||||
{% if request.GET.status == "s" %}selected="selected"{% endif %}>{% trans "Voucher assigned" %}</option>
|
||||
<option value="v"
|
||||
{% if request.GET.status == "v" %}selected="selected"{% endif %}>
|
||||
{% trans "Waiting for redemption" %}</option>
|
||||
<option value="r"
|
||||
{% if request.GET.status == "r" %}selected="selected"{% endif %}>
|
||||
{% trans "Successfully redeemed" %}</option>
|
||||
<option value="e"
|
||||
{% if request.GET.status == "e" %}selected="selected"{% endif %}>
|
||||
{% trans "Voucher expired" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-3 col-xs-6">
|
||||
<select name="item" class="form-control">
|
||||
<option value="">{% trans "All products" %}</option>
|
||||
{% for item in items %}
|
||||
<option value="{{ item.id }}"
|
||||
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
|
||||
{{ item }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if request.event.has_subevents %}
|
||||
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
<a href="?{% url_replace request "download" "yes" %}"
|
||||
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
|
||||
<button class="btn btn-primary" type="submit"><span class="fa fa-filter"></span> {% trans "Filter" %}</button>
|
||||
<a href="?{% url_replace request "download" "yes" %}"
|
||||
class="btn btn-default"><i class="fa fa-download"></i>
|
||||
{% trans "Download list" %}</a>
|
||||
{% trans "Download list" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
</p>
|
||||
<form method="post" action="?next={{ request.get_full_path|urlencode }}">
|
||||
{% csrf_token %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "User" %}</th>
|
||||
{% if request.event.settings.waiting_list_names_asked %}
|
||||
<th>{% trans "Name" %}</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Email" %}</th>
|
||||
{% if request.event.settings.waiting_list_phones_asked %}
|
||||
<th>{% trans "Phone number" %}</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Product" %}</th>
|
||||
{% if request.event.has_subevents %}
|
||||
<th>{% trans "Date" context "subevent" %}</th>
|
||||
@@ -146,7 +144,13 @@
|
||||
<tbody>
|
||||
{% for e in entries %}
|
||||
<tr>
|
||||
{% if request.event.settings.waiting_list_names_asked %}
|
||||
<td>{{ e.name|default:"" }}</td>
|
||||
{% endif %}
|
||||
<td>{{ e.email }}</td>
|
||||
{% if request.event.settings.waiting_list_phones_asked %}
|
||||
<td>{{ e.phone|default:"" }}</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{{ e.item }}
|
||||
{% if e.variation %}
|
||||
@@ -154,7 +158,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if request.event.has_subevents %}
|
||||
<td>{{ e.subevent.name }} – {{ e.subevent.get_date_range_display }}</td>
|
||||
<td>{{ e.subevent.name }} – {{ e.subevent.get_date_range_display }} {{ e.subevent.date_from|date:"TIME_FORMAT" }}</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{{ e.created|date:"SHORT_DATETIME_FORMAT" }}
|
||||
|
||||
@@ -691,14 +691,14 @@ class MailSettingsRendererPreview(MailSettingsPreview):
|
||||
expires=now(), code="PREVIEW", total=119)
|
||||
item = request.event.items.create(name=gettext("Sample product"), default_price=42.23,
|
||||
description=gettext("Sample product description"))
|
||||
p = order.positions.create(item=item, attendee_name_parts={'_legacy': gettext("John Doe")},
|
||||
price=item.default_price)
|
||||
order.positions.create(item=item, attendee_name_parts={'_legacy': gettext("John Doe")},
|
||||
price=item.default_price, subevent=request.event.subevents.last())
|
||||
v = renderers[request.GET.get('renderer')].render(
|
||||
v,
|
||||
str(request.event.settings.mail_text_signature),
|
||||
gettext('Your order: %(code)s') % {'code': order.code},
|
||||
order,
|
||||
position=p
|
||||
position=None
|
||||
)
|
||||
r = HttpResponse(v, content_type='text/html')
|
||||
r._csp_ignore = True
|
||||
@@ -955,11 +955,10 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
|
||||
return reverse('control:index')
|
||||
|
||||
|
||||
class EventLog(EventPermissionRequiredMixin, ListView):
|
||||
class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
template_name = 'pretixcontrol/event/logs.html'
|
||||
model = LogEntry
|
||||
context_object_name = 'logs'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.logentry_set.all().select_related(
|
||||
@@ -1363,7 +1362,7 @@ class QuickSetupView(FormView):
|
||||
tax_rule=tax_rule,
|
||||
admission=True,
|
||||
position=i,
|
||||
sales_channels=[k for k in get_all_sales_channels().keys()]
|
||||
sales_channels=list(get_all_sales_channels().keys())
|
||||
)
|
||||
item.log_action('pretix.event.item.added', user=self.request.user, data=dict(f.cleaned_data))
|
||||
if f.cleaned_data['quota'] or not form.cleaned_data['total_quota']:
|
||||
|
||||
@@ -83,7 +83,7 @@ from pretix.control.forms.orders import (
|
||||
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
|
||||
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
||||
OrderRefundForm, OtherOperationsForm,
|
||||
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.signals import order_search_forms
|
||||
@@ -151,6 +151,11 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
|
||||
s = OrderPosition.objects.filter(
|
||||
order=OuterRef('pk')
|
||||
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||
i = Invoice.objects.filter(
|
||||
order=OuterRef('pk'),
|
||||
is_cancellation=False,
|
||||
refered__isnull=True,
|
||||
).order_by().values('order').annotate(k=Count('id')).values('k')
|
||||
annotated = {
|
||||
o['pk']: o
|
||||
for o in
|
||||
@@ -158,10 +163,11 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
|
||||
pk__in=[o.pk for o in ctx['orders']]
|
||||
).annotate(
|
||||
pcnt=Subquery(s, output_field=IntegerField()),
|
||||
icnt=Subquery(i, output_field=IntegerField()),
|
||||
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk')))
|
||||
).values(
|
||||
'pk', 'pcnt', 'is_overpaid', 'is_underpaid', 'is_pending_with_full_payment', 'has_external_refund',
|
||||
'has_pending_refund', 'has_cancellation_request', 'computed_payment_refund_sum'
|
||||
'has_pending_refund', 'has_cancellation_request', 'computed_payment_refund_sum', 'icnt'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -177,6 +183,7 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
|
||||
o.has_pending_refund = annotated.get(o.pk)['has_pending_refund']
|
||||
o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request']
|
||||
o.computed_payment_refund_sum = annotated.get(o.pk)['computed_payment_refund_sum']
|
||||
o.icnt = annotated.get(o.pk)['icnt']
|
||||
o.sales_channel_obj = scs[o.sales_channel]
|
||||
|
||||
if ctx['page_obj'].paginator.count < 1000:
|
||||
@@ -1134,6 +1141,7 @@ class OrderTransition(OrderView):
|
||||
try:
|
||||
cancel_order(self.order.pk, user=self.request.user,
|
||||
send_mail=self.mark_canceled_form.cleaned_data['send_email'],
|
||||
cancel_invoice=self.mark_canceled_form.cleaned_data.get('cancel_invoice', True),
|
||||
cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'))
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
@@ -1416,11 +1424,24 @@ class OrderExtend(OrderView):
|
||||
class OrderReactivate(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
@cached_property
|
||||
def reactivate_form(self):
|
||||
return ReactivateOrderForm(
|
||||
instance=self.order,
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if not self.reactivate_form.is_valid():
|
||||
return render(self.request, 'pretixcontrol/order/reactivate.html', {
|
||||
'form': self.reactivate_form,
|
||||
'order': self.order,
|
||||
})
|
||||
try:
|
||||
reactivate_order(
|
||||
self.order,
|
||||
user=self.request.user
|
||||
user=self.request.user,
|
||||
force=self.reactivate_form.cleaned_data.get('force', False)
|
||||
)
|
||||
messages.success(self.request, _('The order has been reactivated.'))
|
||||
except OrderError as e:
|
||||
@@ -1445,6 +1466,7 @@ class OrderReactivate(OrderView):
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return render(self.request, 'pretixcontrol/order/reactivate.html', {
|
||||
'form': self.reactivate_form,
|
||||
'order': self.order,
|
||||
})
|
||||
|
||||
|
||||
@@ -938,6 +938,7 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
|
||||
template_name = 'pretixcontrol/organizers/giftcards.html'
|
||||
permission = 'can_manage_gift_cards'
|
||||
context_object_name = 'giftcards'
|
||||
paginate_by = 50
|
||||
|
||||
def get_queryset(self):
|
||||
s = GiftCardTransaction.objects.filter(
|
||||
@@ -1437,12 +1438,11 @@ class EventMetaPropertyDeleteView(OrganizerDetailViewMixin, OrganizerPermissionR
|
||||
return redirect(success_url)
|
||||
|
||||
|
||||
class LogView(OrganizerPermissionRequiredMixin, ListView):
|
||||
class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
template_name = 'pretixcontrol/organizers/logs.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
model = LogEntry
|
||||
context_object_name = 'logs'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.organizer.all_logentries().select_related(
|
||||
|
||||
@@ -1184,7 +1184,7 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
|
||||
for fname in ('size', 'name', 'release_after_exit'):
|
||||
setattr(q, fname, f.cleaned_data.get(fname))
|
||||
q.save(clear_cache=False)
|
||||
if 'itemvar' in f.changed_data:
|
||||
if 'itemvars' in f.changed_data:
|
||||
q.items.set(selected_items)
|
||||
q.variations.set(selected_variations)
|
||||
log_entries.append(
|
||||
|
||||
@@ -255,7 +255,7 @@ def subevent_select2(request, **kwargs):
|
||||
|
||||
qs = request.event.subevents.filter(
|
||||
qf
|
||||
).order_by('-date_from')
|
||||
).order_by('-date_from', 'name', 'pk')
|
||||
|
||||
total = qs.count()
|
||||
pagesize = 20
|
||||
|
||||
@@ -3,7 +3,8 @@ import io
|
||||
from defusedcsv import csv
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import Sum
|
||||
from django.http import (
|
||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
||||
@@ -21,7 +22,9 @@ from django.views.generic import (
|
||||
|
||||
from pretix.base.models import CartPosition, LogEntry, OrderPosition, Voucher
|
||||
from pretix.base.models.vouchers import _generate_random_code
|
||||
from pretix.base.services.locking import NoLockManager
|
||||
from pretix.base.services.vouchers import vouchers_send
|
||||
from pretix.base.views.tasks import AsyncFormView
|
||||
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
|
||||
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
@@ -287,13 +290,19 @@ class VoucherGo(EventPermissionRequiredMixin, View):
|
||||
return redirect('control:event.vouchers', event=request.event.slug, organizer=request.event.organizer.slug)
|
||||
|
||||
|
||||
class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
||||
class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
|
||||
model = Voucher
|
||||
template_name = 'pretixcontrol/vouchers/bulk.html'
|
||||
permission = 'can_change_vouchers'
|
||||
context_object_name = 'voucher'
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
def get_success_url(self, value) -> str:
|
||||
return reverse('control:event.vouchers', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
def get_error_url(self):
|
||||
return reverse('control:event.vouchers', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
@@ -316,34 +325,84 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
||||
i.redeemed = 0
|
||||
kwargs['instance'] = i
|
||||
else:
|
||||
kwargs['instance'] = Voucher(event=self.request.event)
|
||||
kwargs['instance'] = Voucher(event=self.request.event, code=None)
|
||||
return kwargs
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
log_entries = []
|
||||
objs = form.save(self.request.event)
|
||||
def get_async_form_kwargs(self, form_kwargs, organizer=None, event=None):
|
||||
if not form_kwargs.get('instance'):
|
||||
form_kwargs['instance'] = Voucher(event=self.request.event, code=None)
|
||||
return form_kwargs
|
||||
|
||||
def async_form_valid(self, task, form):
|
||||
lockfn = NoLockManager
|
||||
if form.data.get('block_quota'):
|
||||
lockfn = self.request.event.lock
|
||||
batch_size = 500
|
||||
total_num = 1 # will be set later
|
||||
|
||||
def set_progress(percent):
|
||||
if not task.request.called_directly:
|
||||
task.update_state(
|
||||
state='PROGRESS',
|
||||
meta={'value': percent}
|
||||
)
|
||||
|
||||
def process_batch(batch_vouchers, voucherids):
|
||||
Voucher.objects.bulk_create(batch_vouchers)
|
||||
if not connection.features.can_return_rows_from_bulk_insert:
|
||||
batch_vouchers = list(self.request.event.vouchers.filter(code__in=[v.code for v in batch_vouchers]))
|
||||
|
||||
log_entries = []
|
||||
for v in batch_vouchers:
|
||||
voucherids.append(v.pk)
|
||||
data = dict(form.cleaned_data)
|
||||
data['code'] = code
|
||||
data['bulk'] = True
|
||||
del data['codes']
|
||||
log_entries.append(
|
||||
v.log_action('pretix.voucher.added', data=data, user=self.request.user, save=False)
|
||||
)
|
||||
LogEntry.objects.bulk_create(log_entries)
|
||||
form.post_bulk_save(batch_vouchers)
|
||||
batch_vouchers.clear()
|
||||
set_progress(len(voucherids) / total_num * (50. if form.cleaned_data['send'] else 100.))
|
||||
|
||||
voucherids = []
|
||||
for v in objs:
|
||||
log_entries.append(
|
||||
v.log_action('pretix.voucher.added', data=form.cleaned_data, user=self.request.user, save=False)
|
||||
)
|
||||
voucherids.append(v.pk)
|
||||
LogEntry.objects.bulk_create(log_entries, batch_size=200)
|
||||
with lockfn(), transaction.atomic():
|
||||
if not form.is_valid():
|
||||
raise ValidationError(form.errors)
|
||||
total_num = len(form.cleaned_data['codes'])
|
||||
|
||||
batch_vouchers = []
|
||||
for code in form.cleaned_data['codes']:
|
||||
if len(batch_vouchers) > batch_size:
|
||||
process_batch(batch_vouchers, voucherids)
|
||||
|
||||
obj = modelcopy(form.instance, code=None)
|
||||
obj.event = self.request.event
|
||||
obj.code = code
|
||||
try:
|
||||
obj.seat = form.cleaned_data['seats'].pop()
|
||||
obj.item = obj.seat.product
|
||||
except IndexError:
|
||||
pass
|
||||
batch_vouchers.append(obj)
|
||||
|
||||
process_batch(batch_vouchers, voucherids)
|
||||
|
||||
if form.cleaned_data['send']:
|
||||
vouchers_send.apply_async(kwargs={
|
||||
'event': self.request.event.pk,
|
||||
'vouchers': voucherids,
|
||||
'subject': form.cleaned_data['send_subject'],
|
||||
'message': form.cleaned_data['send_message'],
|
||||
'recipients': [r._asdict() for r in form.cleaned_data['send_recipients']],
|
||||
'user': self.request.user.pk,
|
||||
})
|
||||
messages.success(self.request, _('The new vouchers have been created and will be sent out shortly.'))
|
||||
else:
|
||||
messages.success(self.request, _('The new vouchers have been created.'))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
vouchers_send(
|
||||
event=self.request.event,
|
||||
vouchers=voucherids,
|
||||
subject=form.cleaned_data['send_subject'],
|
||||
message=form.cleaned_data['send_message'],
|
||||
recipients=[r._asdict() for r in form.cleaned_data['send_recipients']],
|
||||
user=self.request.user.pk,
|
||||
progress=lambda p: set_progress(50. + p * 50.)
|
||||
)
|
||||
|
||||
def get_success_message(self, value):
|
||||
return _('The new vouchers have been created.')
|
||||
|
||||
def get_form_class(self):
|
||||
form_class = VoucherBulkForm
|
||||
@@ -357,11 +416,6 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
||||
ctx['code_length'] = settings.ENTROPY['voucher_code']
|
||||
return ctx
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# TODO: Transform this into an asynchronous call?
|
||||
with request.event.lock():
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
||||
class VoucherRNG(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_vouchers'
|
||||
|
||||
@@ -211,7 +211,7 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
|
||||
|
||||
headers = [
|
||||
_('E-mail address'), _('Product'), _('On list since'), _('Status'), _('Voucher code'),
|
||||
_('Name'), _('E-mail address'), _('Phone number'), _('Product'), _('On list since'), _('Status'), _('Voucher code'),
|
||||
_('Language'), _('Priority')
|
||||
]
|
||||
if self.request.event.has_subevents:
|
||||
@@ -235,7 +235,9 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
status = _('Waiting')
|
||||
|
||||
row = [
|
||||
w.name,
|
||||
w.email,
|
||||
w.phone,
|
||||
prod,
|
||||
w.created.isoformat(),
|
||||
status,
|
||||
|
||||
@@ -94,7 +94,7 @@ def merge(*args):
|
||||
"""Implements the 'merge' operator for merging lists."""
|
||||
ret = []
|
||||
for arg in args:
|
||||
if isinstance(arg, list) or isinstance(arg, tuple):
|
||||
if isinstance(arg, (list, tuple)):
|
||||
ret += list(arg)
|
||||
else:
|
||||
ret.append(arg)
|
||||
|
||||
@@ -12,8 +12,8 @@ class Thumbnail(models.Model):
|
||||
unique_together = (('source', 'size'),)
|
||||
|
||||
|
||||
def modelcopy(obj: models.Model):
|
||||
n = obj.__class__()
|
||||
def modelcopy(obj: models.Model, **kwargs):
|
||||
n = obj.__class__(**kwargs)
|
||||
for f in obj._meta.fields:
|
||||
val = getattr(obj, f.name)
|
||||
if isinstance(val, models.Model):
|
||||
|
||||
+1353
-1204
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2020-07-30 19:00+0000\n"
|
||||
"Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
+1350
-1202
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
+1358
-1239
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2020-12-14 10:00+0000\n"
|
||||
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
|
||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
+1348
-1202
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2020-09-15 02:00+0000\n"
|
||||
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
+1332
-1209
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2020-08-25 02:00+0000\n"
|
||||
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2020-08-25 02:00+0000\n"
|
||||
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
|
||||
+1344
-1239
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
+1354
-1204
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
|
||||
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
+1353
-1204
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2020-04-27 20:00+0000\n"
|
||||
"Last-Translator: Gonzalo Gabriel Perez <zalitoar@gmail.com>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
+1338
-1205
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2021-01-20 16:10+0000\n"
|
||||
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
+1352
-1204
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: French\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2020-09-15 17:00+0000\n"
|
||||
"Last-Translator: Martin Gross <martin@pc-coholic.de>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
+1348
-1239
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2020-01-24 08:00+0000\n"
|
||||
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
|
||||
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
+1369
-1241
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2020-06-12 20:00+0000\n"
|
||||
"Last-Translator: Frank <webappconcept@gmail.com>\n"
|
||||
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
+1336
-1201
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: 2019-11-13 06:00+0000\n"
|
||||
"Last-Translator: Zane Smite <z.smite@riga-jurmala.com>\n"
|
||||
"Language-Team: Latvian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-26 09:06+0000\n"
|
||||
"POT-Creation-Date: 2021-03-08 16:40+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
+1418
-1365
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user