forked from CGM_Public/pretix_original
Compare commits
73
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 | ||
|
|
a09dac89c4 | ||
|
|
8af91b691d | ||
|
|
2221b57dc9 | ||
|
|
8d99388c08 | ||
|
|
de597ba864 | ||
|
|
2d9a16e94d |
@@ -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/
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.16.0"
|
||||
__version__ = "3.17.0.dev0"
|
||||
|
||||
@@ -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:
|
||||
@@ -596,6 +596,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'checkout_email_helptext',
|
||||
'presale_has_ended_text',
|
||||
'voucher_explanation_text',
|
||||
'checkout_success_text',
|
||||
'banner_text',
|
||||
'banner_text_bottom',
|
||||
'show_dates_on_frontpage',
|
||||
@@ -613,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',
|
||||
@@ -623,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',
|
||||
@@ -656,6 +663,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'mail_from',
|
||||
'mail_from_name',
|
||||
'mail_attach_ical',
|
||||
'mail_attach_tickets',
|
||||
'invoice_address_asked',
|
||||
'invoice_address_required',
|
||||
'invoice_address_vatid',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,6 +18,7 @@ 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,
|
||||
@@ -111,6 +112,8 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
|
||||
if not i.file:
|
||||
invoice_pdf_task.apply(args=(i.pk,))
|
||||
i.refresh_from_db()
|
||||
if not i.file:
|
||||
raise ExportError('Could not generate PDF for invoice {nr}'.format(nr=i.full_invoice_no))
|
||||
i.file.open('rb')
|
||||
zipf.writestr('{}-{}.pdf'.format(i.number, i.order.code), i.file.read())
|
||||
i.file.close()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -255,8 +255,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
invoice_from_top = 17 * mm
|
||||
|
||||
def _draw_invoice_from(self, canvas):
|
||||
p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet[
|
||||
'InvoiceFrom'])
|
||||
p = Paragraph(
|
||||
bleach.clean(self.invoice.full_invoice_from, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
style=self.stylesheet['InvoiceFrom']
|
||||
)
|
||||
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
|
||||
p_size = p.wrap(self.invoice_from_width, self.invoice_from_height)
|
||||
p.drawOn(canvas, self.invoice_from_left, self.pagesize[1] - p_size[1] - self.invoice_from_top)
|
||||
@@ -361,6 +363,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
def _draw_event(self, canvas):
|
||||
def shorten(txt):
|
||||
txt = str(txt)
|
||||
txt = bleach.clean(txt, tags=[]).strip()
|
||||
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
|
||||
@@ -441,13 +444,18 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
story = []
|
||||
if self.invoice.custom_field:
|
||||
story.append(Paragraph(
|
||||
'{}: {}'.format(self.invoice.event.settings.invoice_address_custom_field, self.invoice.custom_field),
|
||||
'{}: {}'.format(
|
||||
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']
|
||||
))
|
||||
|
||||
if self.invoice.internal_reference:
|
||||
story.append(Paragraph(
|
||||
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
|
||||
pgettext('invoice', 'Customer reference: {reference}').format(
|
||||
reference=bleach.clean(self.invoice.internal_reference, tags=[]).strip().replace('\n', '<br />\n'),
|
||||
),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
@@ -466,7 +474,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
))
|
||||
|
||||
if self.invoice.introductory_text:
|
||||
story.append(Paragraph(self.invoice.introductory_text, self.stylesheet['Normal']))
|
||||
story.append(Paragraph(
|
||||
self.invoice.introductory_text,
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
story.append(Spacer(1, 10 * mm))
|
||||
|
||||
return story
|
||||
@@ -566,10 +577,16 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
story.append(Spacer(1, 15 * mm))
|
||||
|
||||
if self.invoice.payment_provider_text:
|
||||
story.append(Paragraph(self.invoice.payment_provider_text, self.stylesheet['Normal']))
|
||||
story.append(Paragraph(
|
||||
self.invoice.payment_provider_text,
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.additional_text:
|
||||
story.append(Paragraph(self.invoice.additional_text, self.stylesheet['Normal']))
|
||||
story.append(Paragraph(
|
||||
self.invoice.additional_text,
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
story.append(Spacer(1, 15 * mm))
|
||||
|
||||
tstyledata = [
|
||||
@@ -701,7 +718,10 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
def _draw_invoice_from(self, canvas):
|
||||
if not self.invoice.invoice_from:
|
||||
return
|
||||
c = self.invoice.address_invoice_from.strip().split('\n')
|
||||
c = [
|
||||
bleach.clean(l, tags=[]).strip().replace('\n', '<br />\n')
|
||||
for l in self.invoice.address_invoice_from.strip().split('\n')
|
||||
]
|
||||
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
|
||||
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
|
||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * 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):
|
||||
@@ -291,6 +300,8 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
||||
order = None
|
||||
else:
|
||||
with language(order.locale, event.settings.region):
|
||||
if not event.settings.mail_attach_tickets:
|
||||
attach_tickets = False
|
||||
if position:
|
||||
try:
|
||||
position = order.positions.get(pk=position)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -10,7 +10,6 @@ from django.db.models import (
|
||||
)
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django_redis import get_redis_connection
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import (
|
||||
@@ -90,7 +89,7 @@ class QuotaAvailability:
|
||||
def queue(self, *quota):
|
||||
self._queue += quota
|
||||
|
||||
def compute(self, now_dt=None, dbcache=True):
|
||||
def compute(self, now_dt=None):
|
||||
now_dt = now_dt or now()
|
||||
quotas = list(set(self._queue))
|
||||
quotas_original = list(self._queue)
|
||||
@@ -107,18 +106,18 @@ class QuotaAvailability:
|
||||
|
||||
self._close(quotas)
|
||||
try:
|
||||
self._write_cache(quotas, now_dt, dbcache)
|
||||
self._write_cache(quotas, now_dt)
|
||||
except OperationalError as e:
|
||||
# Ignore deadlocks when multiple threads try to write to the cache
|
||||
if 'deadlock' not in str(e).lower():
|
||||
raise e
|
||||
|
||||
def _write_cache(self, quotas, now_dt, dbcache):
|
||||
def _write_cache(self, quotas, now_dt):
|
||||
# We used to also delete item_quota_cache:* from the event cache here, but as the cache
|
||||
# gets more complex, this does not seem worth it. The cache is only present for up to
|
||||
# 5 seconds to prevent high peaks, and a 5-second delay in availability is usually
|
||||
# tolerable
|
||||
update = defaultdict(list)
|
||||
update = []
|
||||
for q in quotas:
|
||||
rewrite_cache = self._count_waitinglist and (
|
||||
not q.cache_is_hot(now_dt) or self.results[q][0] > q.cached_availability_state
|
||||
@@ -130,21 +129,12 @@ class QuotaAvailability:
|
||||
q.cached_availability_time = now_dt
|
||||
if q in self.count_paid_orders:
|
||||
q.cached_availability_paid_orders = self.count_paid_orders[q]
|
||||
update[q.event_id].append(q)
|
||||
|
||||
update.append(q)
|
||||
if update:
|
||||
# if dbcache:
|
||||
# Quota.objects.using('default').bulk_update(sum((quotas for event, quotas in update.items()), []), [
|
||||
# 'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
|
||||
# 'cached_availability_paid_orders'
|
||||
# ], batch_size=50)
|
||||
|
||||
if settings.HAS_REDIS:
|
||||
rc = get_redis_connection("redis")
|
||||
for eventid, quotas in update.items():
|
||||
rc.hmset(f'quotas:{eventid}:availability', {
|
||||
str(q.id): ",".join([str(i) for i in self.results[q]]) for q in quotas
|
||||
})
|
||||
Quota.objects.using('default').bulk_update(update, [
|
||||
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
|
||||
'cached_availability_paid_orders'
|
||||
], batch_size=50)
|
||||
|
||||
def _close(self, quotas):
|
||||
for q in quotas:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.core.validators import (
|
||||
MaxValueValidator, MinValueValidator, RegexValidator,
|
||||
)
|
||||
from django.db.models import Model
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.translation import (
|
||||
gettext_lazy as _, gettext_noop, pgettext, pgettext_lazy,
|
||||
)
|
||||
@@ -974,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,
|
||||
@@ -1322,6 +1378,19 @@ DEFAULTS = {
|
||||
'default': 'classic',
|
||||
'type': str
|
||||
},
|
||||
'mail_attach_tickets': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Attach ticket files"),
|
||||
help_text=format_lazy(
|
||||
_("Tickets will never be attached if they're larger than {size} to avoid email delivery problems."),
|
||||
size='4 MB'
|
||||
),
|
||||
)
|
||||
},
|
||||
'mail_attach_ical': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -1730,7 +1799,7 @@ Your {event} team"""))
|
||||
),
|
||||
},
|
||||
'theme_color_danger': {
|
||||
'default': '#D36060',
|
||||
'default': '#C44F4F',
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
@@ -1931,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,
|
||||
@@ -1983,6 +2064,19 @@ Your {event} team"""))
|
||||
"why you need information from them.")
|
||||
)
|
||||
},
|
||||
'checkout_success_text': {
|
||||
'default': '',
|
||||
'type': LazyI18nString,
|
||||
'serializer_class': I18nField,
|
||||
'form_class': I18nFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Additional success message"),
|
||||
help_text=_("This message will be shown after an order has been created successfully. It will be shown in additional "
|
||||
"to the default text."),
|
||||
widget_kwargs={'attrs': {'rows': '2'}},
|
||||
widget=I18nTextarea
|
||||
)
|
||||
},
|
||||
'checkout_phone_helptext': {
|
||||
'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
|
||||
|
||||
|
||||
@@ -435,6 +435,7 @@ class EventSettingsForm(SettingsForm):
|
||||
'checkout_email_helptext',
|
||||
'presale_has_ended_text',
|
||||
'voucher_explanation_text',
|
||||
'checkout_success_text',
|
||||
'show_dates_on_frontpage',
|
||||
'show_date_to',
|
||||
'show_times',
|
||||
@@ -448,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',
|
||||
@@ -458,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',
|
||||
@@ -786,6 +793,7 @@ class MailSettingsForm(SettingsForm):
|
||||
'mail_from',
|
||||
'mail_from_name',
|
||||
'mail_attach_ical',
|
||||
'mail_attach_tickets',
|
||||
]
|
||||
|
||||
mail_sales_channel_placed_paid = forms.MultipleChoiceField(
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
{% bootstrap_field form.mail_from_name layout="control" %}
|
||||
{% bootstrap_field form.mail_text_signature layout="control" %}
|
||||
{% bootstrap_field form.mail_bcc layout="control" %}
|
||||
{% bootstrap_field form.mail_attach_tickets layout="control" %}
|
||||
{% bootstrap_field form.mail_attach_ical layout="control" %}
|
||||
{% bootstrap_field form.mail_sales_channel_placed_paid layout="control" %}
|
||||
</fieldset>
|
||||
|
||||
@@ -188,10 +188,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% bootstrap_field sform.checkout_success_text layout="control" %}
|
||||
{% bootstrap_field sform.checkout_email_helptext layout="control" %}
|
||||
{% 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>
|
||||
@@ -247,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-"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user