forked from CGM_Public/pretix_original
Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f8e6a2a4b | ||
|
|
c084b91ab3 | ||
|
|
8baaa0a8c6 | ||
|
|
53070f5d4b | ||
|
|
5685a349ea | ||
|
|
1af69d5c76 | ||
|
|
adddc7a71e | ||
|
|
11f23c3fd2 | ||
|
|
954fece6cf | ||
|
|
8ef6adc3d5 | ||
|
|
88ba7ab53a | ||
|
|
eae55e4b5a | ||
|
|
5ae839f62e | ||
|
|
7314d32422 | ||
|
|
97d6ae8e55 | ||
|
|
13063cb9d2 | ||
|
|
2792813d95 | ||
|
|
d6aeefdf09 | ||
|
|
13056ef477 | ||
|
|
6e2b5eae9a | ||
|
|
4cfb10b254 | ||
|
|
ebd336e8cb | ||
|
|
1357b010de | ||
|
|
09b2e69178 | ||
|
|
5e34032821 | ||
|
|
46cee890f0 | ||
|
|
4a2ac110b3 | ||
|
|
7eefd3dc59 | ||
|
|
fdca62685c | ||
|
|
7ae38b5e97 | ||
|
|
76e9093fea | ||
|
|
b3c9dca024 | ||
|
|
f4710cf019 | ||
|
|
5f192fd0ce | ||
|
|
a897f60fc5 | ||
|
|
74107781ce | ||
|
|
ad219df7cf | ||
|
|
002ab4aa06 | ||
|
|
a84a726185 | ||
|
|
5f58b93c71 | ||
|
|
3eaaf80c0a | ||
|
|
3b5d811b27 | ||
|
|
f0da2b7233 | ||
|
|
d8d7440b52 | ||
|
|
a1ec9fceb0 | ||
|
|
27ff73255b | ||
|
|
bba103156c | ||
|
|
f1a98b5c30 | ||
|
|
405b3a22e1 | ||
|
|
a51c2a36a6 | ||
|
|
8e00970f04 | ||
|
|
8ca2fe7707 | ||
|
|
b93e2307d0 | ||
|
|
97f3b72254 | ||
|
|
00a77d3de9 | ||
|
|
35d9a0dacf | ||
|
|
d2e6320e1e | ||
|
|
671eb902a8 | ||
|
|
be67059099 | ||
|
|
6e3791a49e | ||
|
|
e3bd665093 | ||
|
|
748e2bb2fa | ||
|
|
b13b34f00d | ||
|
|
641e3216d9 | ||
|
|
c70901c129 | ||
|
|
460d39b8c2 | ||
|
|
a9963aead1 |
@@ -60,6 +60,10 @@ Here is the currently recommended set of commands::
|
|||||||
CREATE INDEX CONCURRENTLY pretix_addidx_ia_company
|
CREATE INDEX CONCURRENTLY pretix_addidx_ia_company
|
||||||
ON pretixbase_invoiceaddress
|
ON pretixbase_invoiceaddress
|
||||||
USING gin (upper("company") gin_trgm_ops);
|
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::
|
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
|
order string Order code of the order this invoice belongs to
|
||||||
is_cancellation boolean ``true``, if this invoice is the cancellation of a
|
is_cancellation boolean ``true``, if this invoice is the cancellation of a
|
||||||
different invoice.
|
different invoice.
|
||||||
invoice_from string Sender address
|
invoice_from_name string Sender address: Name
|
||||||
invoice_to string Receiver address
|
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
|
date date Invoice date
|
||||||
refers string Invoice number of an invoice this invoice refers to
|
refers string Invoice number of an invoice this invoice refers to
|
||||||
(for example a cancellation refers to the invoice it
|
(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
|
lines list of objects The actual invoice contents
|
||||||
├ position integer Number of the line within an invoice.
|
├ position integer Number of the line within an invoice.
|
||||||
├ description string Text representing the invoice line (e.g. product name)
|
├ 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
|
├ gross_value money (string) Price including taxes
|
||||||
├ tax_value money (string) Tax amount included
|
├ tax_value money (string) Tax amount included
|
||||||
├ tax_name string Name of used tax rate (e.g. "VAT")
|
├ 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.
|
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
|
Endpoints
|
||||||
---------
|
---------
|
||||||
@@ -83,8 +130,24 @@ Endpoints
|
|||||||
"number": "SAMPLECONF-00001",
|
"number": "SAMPLECONF-00001",
|
||||||
"order": "ABC12",
|
"order": "ABC12",
|
||||||
"is_cancellation": false,
|
"is_cancellation": false,
|
||||||
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
|
"invoice_from_name": "Big Events LLC",
|
||||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT ID: EU123456789",
|
"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",
|
"date": "2017-12-01",
|
||||||
"refers": null,
|
"refers": null,
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
@@ -97,6 +160,11 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"description": "Budget Ticket",
|
"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",
|
"gross_value": "23.00",
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
"tax_name": "VAT",
|
"tax_name": "VAT",
|
||||||
@@ -148,8 +216,24 @@ Endpoints
|
|||||||
"number": "SAMPLECONF-00001",
|
"number": "SAMPLECONF-00001",
|
||||||
"order": "ABC12",
|
"order": "ABC12",
|
||||||
"is_cancellation": false,
|
"is_cancellation": false,
|
||||||
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
|
"invoice_from_name": "Big Events LLC",
|
||||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT ID: EU123456789",
|
"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",
|
"date": "2017-12-01",
|
||||||
"refers": null,
|
"refers": null,
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
@@ -162,6 +246,11 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"description": "Budget Ticket",
|
"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",
|
"gross_value": "23.00",
|
||||||
"tax_value": "0.00",
|
"tax_value": "0.00",
|
||||||
"tax_name": "VAT",
|
"tax_name": "VAT",
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ Field Type Description
|
|||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
id integer Internal ID of the waiting list entry
|
id integer Internal ID of the waiting list entry
|
||||||
created datetime Creation date 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
|
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
|
voucher integer Internal ID of the voucher sent to this user. If
|
||||||
this field is set, the user has been sent a voucher
|
this field is set, the user has been sent a voucher
|
||||||
and is no longer waiting. If it is ``null``, the
|
and is no longer waiting. If it is ``null``, the
|
||||||
|
|||||||
+7
-2
@@ -6,8 +6,8 @@ localecompile:
|
|||||||
./manage.py compilemessages
|
./manage.py compilemessages
|
||||||
|
|
||||||
localegen:
|
localegen:
|
||||||
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" $(LNGS)
|
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" --ignore "pretix/static/npm_dir/*" $(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 -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
|
staticfiles: jsi18n
|
||||||
./manage.py collectstatic --noinput
|
./manage.py collectstatic --noinput
|
||||||
@@ -23,3 +23,8 @@ test:
|
|||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
coverage run -m py.test
|
coverage run -m py.test
|
||||||
|
|
||||||
|
npminstall:
|
||||||
|
mkdir -p pretix/static.dist/node_prefix
|
||||||
|
npm install --prefix=pretix/static.dist/node_prefix pretix/static/npm_dir/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class FullAccessSecurityProfile:
|
|||||||
|
|
||||||
|
|
||||||
class AllowListSecurityProfile:
|
class AllowListSecurityProfile:
|
||||||
allowlist = tuple()
|
allowlist = ()
|
||||||
|
|
||||||
def is_allowed(self, request):
|
def is_allowed(self, request):
|
||||||
key = (request.method, f"{request.resolver_match.namespace}:{request.resolver_match.url_name}")
|
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:taxrule-list'),
|
||||||
('GET', 'api-v1:ticketlayout-list'),
|
('GET', 'api-v1:ticketlayout-list'),
|
||||||
('GET', 'api-v1:ticketlayoutitem-list'),
|
('GET', 'api-v1:ticketlayoutitem-list'),
|
||||||
|
('GET', 'api-v1:badgelayout-list'),
|
||||||
|
('GET', 'api-v1:badgeitem-list'),
|
||||||
('GET', 'api-v1:order-list'),
|
('GET', 'api-v1:order-list'),
|
||||||
('POST', 'api-v1:order-list'),
|
('POST', 'api-v1:order-list'),
|
||||||
('GET', 'api-v1:order-detail'),
|
('GET', 'api-v1:order-detail'),
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
# Item Meta properties
|
# Item Meta properties
|
||||||
if item_meta_properties is not None:
|
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():
|
for key, value in item_meta_properties.items():
|
||||||
prop = self.item_meta_props.get(key)
|
prop = self.item_meta_props.get(key)
|
||||||
if prop in current:
|
if prop in current:
|
||||||
@@ -614,6 +614,11 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'waiting_list_enabled',
|
'waiting_list_enabled',
|
||||||
'waiting_list_hours',
|
'waiting_list_hours',
|
||||||
'waiting_list_auto',
|
'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',
|
'max_items_per_order',
|
||||||
'reservation_time',
|
'reservation_time',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
@@ -624,6 +629,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'frontpage_subevent_ordering',
|
'frontpage_subevent_ordering',
|
||||||
'event_list_type',
|
'event_list_type',
|
||||||
'frontpage_text',
|
'frontpage_text',
|
||||||
|
'event_info_text',
|
||||||
'attendee_names_asked',
|
'attendee_names_asked',
|
||||||
'attendee_names_required',
|
'attendee_names_required',
|
||||||
'attendee_emails_asked',
|
'attendee_emails_asked',
|
||||||
|
|||||||
@@ -18,18 +18,18 @@ class FormFieldWrapperField(serializers.Field):
|
|||||||
|
|
||||||
|
|
||||||
simple_mappings = (
|
simple_mappings = (
|
||||||
(forms.DateField, serializers.DateField, tuple()),
|
(forms.DateField, serializers.DateField, ()),
|
||||||
(forms.TimeField, serializers.TimeField, tuple()),
|
(forms.TimeField, serializers.TimeField, ()),
|
||||||
(forms.SplitDateTimeField, serializers.DateTimeField, tuple()),
|
(forms.SplitDateTimeField, serializers.DateTimeField, ()),
|
||||||
(forms.DateTimeField, serializers.DateTimeField, tuple()),
|
(forms.DateTimeField, serializers.DateTimeField, ()),
|
||||||
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
|
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
|
||||||
(forms.FloatField, serializers.FloatField, tuple()),
|
(forms.FloatField, serializers.FloatField, ()),
|
||||||
(forms.IntegerField, serializers.IntegerField, tuple()),
|
(forms.IntegerField, serializers.IntegerField, ()),
|
||||||
(forms.EmailField, serializers.EmailField, tuple()),
|
(forms.EmailField, serializers.EmailField, ()),
|
||||||
(forms.UUIDField, serializers.UUIDField, tuple()),
|
(forms.UUIDField, serializers.UUIDField, ()),
|
||||||
(forms.URLField, serializers.URLField, tuple()),
|
(forms.URLField, serializers.URLField, ()),
|
||||||
(forms.NullBooleanField, serializers.NullBooleanField, tuple()),
|
(forms.NullBooleanField, serializers.NullBooleanField, ()),
|
||||||
(forms.BooleanField, serializers.BooleanField, tuple()),
|
(forms.BooleanField, serializers.BooleanField, ()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,14 @@ class CompatibleCountryField(serializers.Field):
|
|||||||
return instance.country_old
|
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):
|
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||||
country = CompatibleCountryField(source='*')
|
country = CompatibleCountryField(source='*')
|
||||||
name = serializers.CharField(required=False)
|
name = serializers.CharField(required=False)
|
||||||
@@ -1322,17 +1330,24 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceLine
|
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):
|
class InvoiceSerializer(I18nAwareModelSerializer):
|
||||||
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
|
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)
|
lines = InlineInvoiceLineSerializer(many=True)
|
||||||
|
invoice_to_country = CountryField()
|
||||||
|
invoice_from_country = CountryField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Invoice
|
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',
|
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
|
||||||
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
|
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
|
||||||
'internal_reference')
|
'internal_reference')
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class WaitingListSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WaitingListEntry
|
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')
|
read_only_fields = ('id', 'created', 'voucher')
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@@ -32,4 +32,11 @@ class WaitingListSerializer(I18nAwareModelSerializer):
|
|||||||
if availability[0] == 100:
|
if availability[0] == 100:
|
||||||
raise ValidationError("This product is currently available.")
|
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
|
return data
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class InitializeView(APIView):
|
class InitializeView(APIView):
|
||||||
authentication_classes = tuple()
|
authentication_classes = ()
|
||||||
permission_classes = tuple()
|
permission_classes = ()
|
||||||
|
|
||||||
def post(self, request, format=None):
|
def post(self, request, format=None):
|
||||||
serializer = InitializationRequestSerializer(data=request.data)
|
serializer = InitializationRequestSerializer(data=request.data)
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import inspect
|
|||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from itertools import groupby
|
||||||
from smtplib import SMTPResponseException
|
from smtplib import SMTPResponseException
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.mail.backends.smtp import EmailBackend
|
from django.core.mail.backends.smtp import EmailBackend
|
||||||
|
from django.db.models import Count
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
@@ -128,9 +130,21 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
|||||||
|
|
||||||
if order:
|
if order:
|
||||||
htmlctx['order'] = 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:
|
if position:
|
||||||
htmlctx['position'] = position
|
htmlctx['position'] = position
|
||||||
|
htmlctx['ev'] = position.subevent or self.event
|
||||||
|
|
||||||
tpl = get_template(self.template_name)
|
tpl = get_template(self.template_name)
|
||||||
body_html = inline_css(tpl.render(htmlctx))
|
body_html = inline_css(tpl.render(htmlctx))
|
||||||
@@ -237,6 +251,8 @@ def get_email_context(**kwargs):
|
|||||||
from pretix.base.models import InvoiceAddress
|
from pretix.base.models import InvoiceAddress
|
||||||
|
|
||||||
event = kwargs['event']
|
event = kwargs['event']
|
||||||
|
if 'position' in kwargs:
|
||||||
|
kwargs.setdefault("position_or_address", kwargs['position'])
|
||||||
if 'order' in kwargs:
|
if 'order' in kwargs:
|
||||||
try:
|
try:
|
||||||
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ from django.utils.functional import cached_property
|
|||||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext
|
from django.utils.translation import gettext, gettext_lazy as _, pgettext
|
||||||
|
|
||||||
from pretix.base.models import Invoice, InvoiceLine, OrderPayment
|
from pretix.base.models import Invoice, InvoiceLine, OrderPayment
|
||||||
from ..services.export import ExportError
|
|
||||||
|
|
||||||
from ...control.forms.filter import get_all_payment_providers
|
from ...control.forms.filter import get_all_payment_providers
|
||||||
from ...helpers import GroupConcat
|
from ...helpers import GroupConcat
|
||||||
from ...helpers.iter import chunked_iterable
|
from ...helpers.iter import chunked_iterable
|
||||||
from ..exporter import BaseExporter, MultiSheetListExporter
|
from ..exporter import BaseExporter, MultiSheetListExporter
|
||||||
|
from ..services.export import ExportError
|
||||||
from ..services.invoices import invoice_pdf_task
|
from ..services.invoices import invoice_pdf_task
|
||||||
from ..signals import (
|
from ..signals import (
|
||||||
register_data_exporters, register_multievent_data_exporters,
|
register_data_exporters, register_multievent_data_exporters,
|
||||||
|
|||||||
@@ -691,13 +691,18 @@ class QuotaListExporter(ListExporter):
|
|||||||
verbose_name = gettext_lazy('Quota availabilities')
|
verbose_name = gettext_lazy('Quota availabilities')
|
||||||
|
|
||||||
def iterate_list(self, form_data):
|
def iterate_list(self, form_data):
|
||||||
|
has_subevents = self.event.has_subevents
|
||||||
headers = [
|
headers = [
|
||||||
_('Quota name'), _('Total quota'), _('Paid orders'), _('Pending orders'), _('Blocking vouchers'),
|
_('Quota name'), _('Total quota'), _('Paid orders'), _('Pending orders'), _('Blocking vouchers'),
|
||||||
_('Current user\'s carts'), _('Waiting list'), _('Exited orders'), _('Current availability')
|
_('Current user\'s carts'), _('Waiting list'), _('Exited orders'), _('Current availability')
|
||||||
]
|
]
|
||||||
|
if has_subevents:
|
||||||
|
headers.append(pgettext('subevent', 'Date'))
|
||||||
|
headers.append(_('Start date'))
|
||||||
|
headers.append(_('End date'))
|
||||||
yield headers
|
yield headers
|
||||||
|
|
||||||
quotas = list(self.event.quotas.all())
|
quotas = list(self.event.quotas.select_related('subevent'))
|
||||||
qa = QuotaAvailability(full_results=True)
|
qa = QuotaAvailability(full_results=True)
|
||||||
qa.queue(*quotas)
|
qa.queue(*quotas)
|
||||||
qa.compute()
|
qa.compute()
|
||||||
@@ -715,6 +720,18 @@ class QuotaListExporter(ListExporter):
|
|||||||
qa.count_exited_orders[quota],
|
qa.count_exited_orders[quota],
|
||||||
_('Infinite') if avail[1] is None else avail[1]
|
_('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
|
yield row
|
||||||
|
|
||||||
def get_filename(self):
|
def get_filename(self):
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ class WaitingListExporter(ListExporter):
|
|||||||
|
|
||||||
headers = [
|
headers = [
|
||||||
_('Date'),
|
_('Date'),
|
||||||
|
_('Name'),
|
||||||
_('Email'),
|
_('Email'),
|
||||||
|
_('Phone number'),
|
||||||
_('Product name'),
|
_('Product name'),
|
||||||
_('Variation'),
|
_('Variation'),
|
||||||
_('Event slug'),
|
_('Event slug'),
|
||||||
@@ -117,7 +119,9 @@ class WaitingListExporter(ListExporter):
|
|||||||
|
|
||||||
row = [
|
row = [
|
||||||
entry.created.astimezone(tz).strftime(datetime_format), # alternative: .isoformat(),
|
entry.created.astimezone(tz).strftime(datetime_format), # alternative: .isoformat(),
|
||||||
|
entry.name,
|
||||||
entry.email,
|
entry.email,
|
||||||
|
entry.phone,
|
||||||
str(entry.item) if entry.item else "",
|
str(entry.item) if entry.item else "",
|
||||||
str(entry.variation) if entry.variation else "",
|
str(entry.variation) if entry.variation else "",
|
||||||
entry.event.slug,
|
entry.event.slug,
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class NamePartsWidget(forms.MultiWidget):
|
|||||||
if not isinstance(value, list):
|
if not isinstance(value, list):
|
||||||
value = self.decompress(value)
|
value = self.decompress(value)
|
||||||
output = []
|
output = []
|
||||||
final_attrs = self.build_attrs(attrs or dict())
|
final_attrs = self.build_attrs(attrs or {})
|
||||||
if 'required' in final_attrs:
|
if 'required' in final_attrs:
|
||||||
del final_attrs['required']
|
del final_attrs['required']
|
||||||
id_ = final_attrs.get('id', None)
|
id_ = final_attrs.get('id', None)
|
||||||
@@ -122,6 +122,8 @@ class NamePartsWidget(forms.MultiWidget):
|
|||||||
these_attrs.pop('data-no-required-attr', None)
|
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['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]
|
these_attrs['data-size'] = self.scheme['fields'][i][2]
|
||||||
|
if len(self.widgets) > 1:
|
||||||
|
these_attrs['aria-label'] = self.scheme['fields'][i][1]
|
||||||
else:
|
else:
|
||||||
these_attrs = final_attrs
|
these_attrs = final_attrs
|
||||||
output.append(widget.render(name + '_%s' % i, widget_value, these_attrs, renderer=renderer))
|
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)
|
country_name = locale.territories.get(country_code)
|
||||||
if country_name:
|
if country_name:
|
||||||
choices.append((prefix, "{} {}".format(country_name, prefix)))
|
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):
|
def render(self, name, value, *args, **kwargs):
|
||||||
return super().render(name, value or self.initial, *args, **kwargs)
|
return super().render(name, value or self.initial, *args, **kwargs)
|
||||||
@@ -243,7 +245,10 @@ class WrappedPhonePrefixSelect(Select):
|
|||||||
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
||||||
|
|
||||||
def __init__(self, attrs=None, initial=None):
|
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)
|
super(PhoneNumberPrefixWidget, self).__init__(widgets, attrs)
|
||||||
|
|
||||||
def render(self, name, value, attrs=None, renderer=None):
|
def render(self, name, value, attrs=None, renderer=None):
|
||||||
|
|||||||
@@ -445,7 +445,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
if self.invoice.custom_field:
|
if self.invoice.custom_field:
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
'{}: {}'.format(
|
'{}: {}'.format(
|
||||||
bleach.clean(self.invoice.event.settings.invoice_address_custom_field, tags=[]).strip().replace('\n', '<br />\n'),
|
bleach.clean(str(self.invoice.event.settings.invoice_address_custom_field), tags=[]).strip().replace('\n', '<br />\n'),
|
||||||
bleach.clean(self.invoice.custom_field, tags=[]).strip().replace('\n', '<br />\n'),
|
bleach.clean(self.invoice.custom_field, tags=[]).strip().replace('\n', '<br />\n'),
|
||||||
),
|
),
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
@@ -475,7 +475,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
if self.invoice.introductory_text:
|
if self.invoice.introductory_text:
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
bleach.clean(self.invoice.introductory_text, tags=[]).strip().replace('\n', '<br />\n'),
|
self.invoice.introductory_text,
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
story.append(Spacer(1, 10 * mm))
|
story.append(Spacer(1, 10 * mm))
|
||||||
@@ -578,13 +578,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
|||||||
|
|
||||||
if self.invoice.payment_provider_text:
|
if self.invoice.payment_provider_text:
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
bleach.clean(self.invoice.payment_provider_text, tags=[]).strip().replace('\n', '<br />\n'),
|
self.invoice.payment_provider_text,
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
|
|
||||||
if self.invoice.additional_text:
|
if self.invoice.additional_text:
|
||||||
story.append(Paragraph(
|
story.append(Paragraph(
|
||||||
bleach.clean(self.invoice.additional_text, tags=[]).strip().replace('\n', '<br />\n'),
|
self.invoice.additional_text,
|
||||||
self.stylesheet['Normal']
|
self.stylesheet['Normal']
|
||||||
))
|
))
|
||||||
story.append(Spacer(1, 15 * mm))
|
story.append(Spacer(1, 15 * mm))
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class Command(BaseCommand):
|
|||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
|
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
|
||||||
'(dotted path, comma separation)')
|
'(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):
|
def handle(self, *args, **options):
|
||||||
verbosity = int(options['verbosity'])
|
verbosity = int(options['verbosity'])
|
||||||
@@ -28,6 +30,9 @@ class Command(BaseCommand):
|
|||||||
if options.get('tasks'):
|
if options.get('tasks'):
|
||||||
if name not in options.get('tasks').split(','):
|
if name not in options.get('tasks').split(','):
|
||||||
continue
|
continue
|
||||||
|
if options.get('exclude'):
|
||||||
|
if name in options.get('exclude').split(','):
|
||||||
|
continue
|
||||||
|
|
||||||
if verbosity > 1:
|
if verbosity > 1:
|
||||||
self.stdout.write(f'INFO Running {name}…')
|
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():
|
for k, v in rules.items():
|
||||||
if k == 'lookup':
|
if k == 'lookup':
|
||||||
if v[0] == 'product':
|
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':
|
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:
|
else:
|
||||||
_walk_rules(v)
|
_walk_rules(v)
|
||||||
elif isinstance(rules, list):
|
elif isinstance(rules, list):
|
||||||
|
|||||||
@@ -273,6 +273,14 @@ class InvoiceLine(models.Model):
|
|||||||
:type subevent: SubEvent
|
:type subevent: SubEvent
|
||||||
:param event_date_from: Event date of the (sub)event at the time the invoice was created
|
:param event_date_from: Event date of the (sub)event at the time the invoice was created
|
||||||
:type event_date_from: datetime
|
: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)
|
invoice = models.ForeignKey('Invoice', related_name='lines', on_delete=models.CASCADE)
|
||||||
position = models.PositiveIntegerField(default=0)
|
position = models.PositiveIntegerField(default=0)
|
||||||
@@ -283,6 +291,10 @@ class InvoiceLine(models.Model):
|
|||||||
tax_name = models.CharField(max_length=190)
|
tax_name = models.CharField(max_length=190)
|
||||||
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
||||||
event_date_from = models.DateTimeField(null=True)
|
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
|
@property
|
||||||
def net_value(self):
|
def net_value(self):
|
||||||
|
|||||||
@@ -983,6 +983,9 @@ class QuestionAnswer(models.Model):
|
|||||||
|
|
||||||
objects = ScopedManager(organizer='question__event__organizer')
|
objects = ScopedManager(organizer='question__event__organizer')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = [['orderposition', 'question'], ['cartposition', 'question']]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def backend_file_url(self):
|
def backend_file_url(self):
|
||||||
if self.file:
|
if self.file:
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ from django.db import models, transaction
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes import ScopedManager
|
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.email import get_email_context
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import Voucher
|
from pretix.base.models import Voucher
|
||||||
from pretix.base.services.mail import mail
|
from pretix.base.services.mail import mail
|
||||||
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
|
|
||||||
from .base import LoggedModel
|
from .base import LoggedModel
|
||||||
from .event import Event, SubEvent
|
from .event import Event, SubEvent
|
||||||
@@ -37,9 +40,21 @@ class WaitingListEntry(LoggedModel):
|
|||||||
verbose_name=_("On waiting list since"),
|
verbose_name=_("On waiting list since"),
|
||||||
auto_now_add=True
|
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(
|
email = models.EmailField(
|
||||||
verbose_name=_("E-mail address")
|
verbose_name=_("E-mail address")
|
||||||
)
|
)
|
||||||
|
phone = PhoneNumberField(
|
||||||
|
null=True, blank=True,
|
||||||
|
verbose_name=_("Phone number")
|
||||||
|
)
|
||||||
voucher = models.ForeignKey(
|
voucher = models.ForeignKey(
|
||||||
'Voucher',
|
'Voucher',
|
||||||
verbose_name=_("Assigned voucher"),
|
verbose_name=_("Assigned voucher"),
|
||||||
@@ -83,6 +98,27 @@ class WaitingListEntry(LoggedModel):
|
|||||||
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
|
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
|
||||||
WaitingListEntry.clean_subevent(self.event, self.subevent)
|
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):
|
def send_voucher(self, quota_cache=None, user=None, auth=None):
|
||||||
availability = (
|
availability = (
|
||||||
self.variation.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache)
|
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'),
|
label=_('Payment process description in order confirmation emails'),
|
||||||
help_text=_('This text will be included for the {payment_info} placeholder in order confirmation '
|
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 '
|
'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,
|
widget=I18nTextarea,
|
||||||
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
|
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
|
||||||
)),
|
)),
|
||||||
('pending_description', I18nFormField(
|
('pending_description', I18nFormField(
|
||||||
label=_('Payment process description for pending orders'),
|
label=_('Payment process description for pending orders'),
|
||||||
help_text=_('This text will be shown on the order confirmation page 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 '
|
'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,
|
widget=I18nTextarea,
|
||||||
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
|
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
|
||||||
)),
|
)),
|
||||||
] + list(super().settings_form_fields.items())
|
] + list(super().settings_form_fields.items())
|
||||||
)
|
)
|
||||||
@@ -1001,21 +1001,24 @@ class ManualPayment(BasePaymentProvider):
|
|||||||
def checkout_confirm_render(self, request):
|
def checkout_confirm_render(self, request):
|
||||||
return self.payment_form_render(request)
|
return self.payment_form_render(request)
|
||||||
|
|
||||||
def format_map(self, order):
|
def format_map(self, order, payment):
|
||||||
return {
|
return {
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'total': order.total,
|
'amount': payment.amount,
|
||||||
'currency': self.event.currency,
|
'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:
|
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))
|
msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order, payment))
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
def payment_pending_render(self, request, payment) -> str:
|
def payment_pending_render(self, request, payment) -> str:
|
||||||
return rich_text(
|
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"),
|
"editor_sample": _("Event organizer info text"),
|
||||||
"evaluate": lambda op, order, ev: str(order.event.settings.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", {
|
("now_date", {
|
||||||
"label": _("Printing date"),
|
"label": _("Printing date"),
|
||||||
"editor_sample": _("2017-05-31"),
|
"editor_sample": _("2017-05-31"),
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ class CartManager:
|
|||||||
raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,))
|
raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,))
|
||||||
|
|
||||||
def _check_item_constraints(self, op, current_ops=[]):
|
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 (
|
if not (
|
||||||
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
|
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
|
||||||
(isinstance(op, self.ExtendOperation) and op.position.is_bundled)
|
(isinstance(op, self.ExtendOperation) and op.position.is_bundled)
|
||||||
@@ -863,7 +863,7 @@ class CartManager:
|
|||||||
op.position.addons.all().delete()
|
op.position.addons.all().delete()
|
||||||
op.position.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
|
# Create a CartPosition for as much items as we can
|
||||||
requested_count = quota_available_count = voucher_available_count = op.count
|
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:
|
if invoice.event.has_subevents:
|
||||||
desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent)
|
desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent)
|
||||||
InvoiceLine.objects.create(
|
InvoiceLine.objects.create(
|
||||||
position=i, invoice=invoice, description=desc,
|
position=i,
|
||||||
gross_value=p.price, tax_value=p.tax_value,
|
invoice=invoice,
|
||||||
subevent=p.subevent, event_date_from=(p.subevent.date_from if p.subevent else invoice.event.date_from),
|
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 ''
|
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,
|
invoice=invoice,
|
||||||
description=fee_title,
|
description=fee_title,
|
||||||
gross_value=fee.value,
|
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_value=fee.tax_value,
|
||||||
tax_rate=fee.tax_rate,
|
tax_rate=fee.tax_rate,
|
||||||
tax_name=fee.tax_rule.name if fee.tax_rule else ''
|
tax_name=fee.tax_rule.name if fee.tax_rule else ''
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from django.core.mail import (
|
|||||||
EmailMultiAlternatives, SafeMIMEMultipart, get_connection,
|
EmailMultiAlternatives, SafeMIMEMultipart, get_connection,
|
||||||
)
|
)
|
||||||
from django.core.mail.message import SafeMIMEText
|
from django.core.mail.message import SafeMIMEText
|
||||||
|
from django.db import transaction
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.translation import gettext as _, pgettext
|
from django.utils.translation import gettext as _, pgettext
|
||||||
from django_scopes import scope, scopes_disabled
|
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 = []
|
||||||
|
|
||||||
task_chain.append(send_task)
|
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):
|
class CustomEmail(EmailMultiAlternatives):
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
|||||||
|
|
||||||
|
|
||||||
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None,
|
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
|
Mark this order as canceled
|
||||||
:param order: The order to change
|
: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():
|
if not order.cancel_allowed():
|
||||||
raise OrderError(_('You cannot cancel this order.'))
|
raise OrderError(_('You cannot cancel this order.'))
|
||||||
invoices = []
|
invoices = []
|
||||||
i = order.invoices.filter(is_cancellation=False).last()
|
if cancel_invoice:
|
||||||
if i and not i.refered.exists():
|
i = order.invoices.filter(is_cancellation=False).last()
|
||||||
invoices.append(generate_cancellation(i))
|
if i and not i.refered.exists():
|
||||||
|
invoices.append(generate_cancellation(i))
|
||||||
|
|
||||||
for position in order.positions.all():
|
for position in order.positions.all():
|
||||||
for gc in position.issued_gift_cards.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.cancellation_date = now()
|
||||||
order.save(update_fields=['status', 'cancellation_date', 'total'])
|
order.save(update_fields=['status', 'cancellation_date', 'total'])
|
||||||
|
|
||||||
if i:
|
if cancel_invoice and i:
|
||||||
invoices.append(generate_invoice(order))
|
invoices.append(generate_invoice(order))
|
||||||
else:
|
else:
|
||||||
with order.event.lock():
|
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,))
|
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
|
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:
|
||||||
try:
|
try:
|
||||||
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
|
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
|
||||||
cancellation_fee)
|
cancellation_fee, cancel_invoice=cancel_invoice)
|
||||||
if try_auto_refund:
|
if try_auto_refund:
|
||||||
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard,
|
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard,
|
||||||
comment=comment)
|
comment=comment)
|
||||||
|
|||||||
@@ -3,22 +3,21 @@ from i18nfield.strings import LazyI18nString
|
|||||||
|
|
||||||
from pretix.base.email import get_email_context
|
from pretix.base.email import get_email_context
|
||||||
from pretix.base.i18n import language
|
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.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,
|
||||||
def vouchers_send(event: Event, vouchers: list, subject: str, message: str, recipients: list, user: int) -> None:
|
progress=None) -> None:
|
||||||
vouchers = list(Voucher.objects.filter(id__in=vouchers).order_by('id'))
|
vouchers = list(Voucher.objects.filter(id__in=vouchers).order_by('id'))
|
||||||
user = User.objects.get(pk=user)
|
user = User.objects.get(pk=user)
|
||||||
for r in recipients:
|
for ir, r in enumerate(recipients):
|
||||||
voucher_list = []
|
voucher_list = []
|
||||||
for i in range(r['number']):
|
for i in range(r['number']):
|
||||||
voucher_list.append(vouchers.pop())
|
voucher_list.append(vouchers.pop())
|
||||||
with language(event.settings.locale):
|
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(
|
mail(
|
||||||
r['email'],
|
r['email'],
|
||||||
subject,
|
subject,
|
||||||
@@ -27,14 +26,14 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
|||||||
event,
|
event,
|
||||||
locale=event.settings.locale,
|
locale=event.settings.locale,
|
||||||
)
|
)
|
||||||
|
logs = []
|
||||||
for v in voucher_list:
|
for v in voucher_list:
|
||||||
if r.get('tag') and r.get('tag') != v.tag:
|
if r.get('tag') and r.get('tag') != v.tag:
|
||||||
v.tag = r.get('tag')
|
v.tag = r.get('tag')
|
||||||
if v.comment:
|
if v.comment:
|
||||||
v.comment += '\n\n'
|
v.comment += '\n\n'
|
||||||
v.comment = gettext('The voucher has been sent to {recipient}.').format(recipient=r['email'])
|
v.comment = gettext('The voucher has been sent to {recipient}.').format(recipient=r['email'])
|
||||||
v.save(update_fields=['tag', 'comment'])
|
logs.append(v.log_action(
|
||||||
v.log_action(
|
|
||||||
'pretix.voucher.sent',
|
'pretix.voucher.sent',
|
||||||
user=user,
|
user=user,
|
||||||
data={
|
data={
|
||||||
@@ -42,5 +41,11 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
|||||||
'name': r.get('name'),
|
'name': r.get('name'),
|
||||||
'subject': subject,
|
'subject': subject,
|
||||||
'message': message,
|
'message': message,
|
||||||
}
|
},
|
||||||
)
|
save=False
|
||||||
|
))
|
||||||
|
Voucher.objects.bulk_update(voucher_list, fields=['comment', 'tag'], batch_size=500)
|
||||||
|
LogEntry.objects.bulk_create(logs, batch_size=500)
|
||||||
|
|
||||||
|
if progress and ir % 50 == 0:
|
||||||
|
progress(ir / len(recipients))
|
||||||
|
|||||||
@@ -975,6 +975,61 @@ DEFAULTS = {
|
|||||||
widget=forms.NumberInput(),
|
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': {
|
'ticket_download': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
@@ -1744,7 +1799,7 @@ Your {event} team"""))
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
'theme_color_danger': {
|
'theme_color_danger': {
|
||||||
'default': '#D36060',
|
'default': '#C44F4F',
|
||||||
'type': str,
|
'type': str,
|
||||||
'form_class': forms.CharField,
|
'form_class': forms.CharField,
|
||||||
'serializer_class': serializers.CharField,
|
'serializer_class': serializers.CharField,
|
||||||
@@ -1945,6 +2000,18 @@ Your {event} team"""))
|
|||||||
widget=I18nTextarea
|
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': {
|
'banner_text': {
|
||||||
'default': '',
|
'default': '',
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ class EmailAddressShredder(BaseDataShredder):
|
|||||||
class WaitingListShredder(BaseDataShredder):
|
class WaitingListShredder(BaseDataShredder):
|
||||||
verbose_name = _('Waiting list')
|
verbose_name = _('Waiting list')
|
||||||
identifier = '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]]:
|
def generate_files(self) -> List[Tuple[str, str, str]]:
|
||||||
yield 'waiting-list.json', 'application/json', json.dumps([
|
yield 'waiting-list.json', 'application/json', json.dumps([
|
||||||
@@ -213,7 +213,7 @@ class WaitingListShredder(BaseDataShredder):
|
|||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def shred_data(self):
|
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):
|
for wle in self.event.waitinglistentries.select_related('voucher').filter(voucher__isnull=False):
|
||||||
if '@' in wle.voucher.comment:
|
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=""):
|
for le in self.event.logentry_set.filter(action_type="pretix.voucher.added.waitinglist").exclude(data=""):
|
||||||
d = le.parsed_data
|
d = le.parsed_data
|
||||||
|
if 'name' in d:
|
||||||
|
d['name'] = '█'
|
||||||
|
if 'name_parts' in d:
|
||||||
|
d['name_parts'] = {
|
||||||
|
'_legacy': '█'
|
||||||
|
}
|
||||||
d['email'] = '█'
|
d['email'] = '█'
|
||||||
|
d['phone'] = '█'
|
||||||
le.data = json.dumps(d)
|
le.data = json.dumps(d)
|
||||||
le.shredded = True
|
le.shredded = True
|
||||||
le.save(update_fields=['data', 'shredded'])
|
le.save(update_fields=['data', 'shredded'])
|
||||||
|
|||||||
@@ -136,6 +136,31 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: {{ color }};
|
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 %}
|
{% if rtl %}
|
||||||
body {
|
body {
|
||||||
direction: rtl;
|
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>
|
<table cellpadding="20"><tr><td>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{% if position %}
|
{% include "pretixbase/email/order_details.html" %}
|
||||||
{% 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 %}
|
|
||||||
</div>
|
</div>
|
||||||
<!--[if gte mso 9]>
|
<!--[if gte mso 9]>
|
||||||
</td></tr></table>
|
</td></tr></table>
|
||||||
|
|||||||
@@ -147,6 +147,31 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: {{ color }};
|
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 %}
|
{% if rtl %}
|
||||||
body {
|
body {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
@@ -226,23 +251,7 @@
|
|||||||
<table cellpadding="20"><tr><td>
|
<table cellpadding="20"><tr><td>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{% if position %}
|
{% include "pretixbase/email/order_details.html" %}
|
||||||
{% 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 %}
|
|
||||||
</div>
|
</div>
|
||||||
<!--[if gte mso 9]>
|
<!--[if gte mso 9]>
|
||||||
</td></tr></table>
|
</td></tr></table>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% load thumb %}
|
{% load thumb %}
|
||||||
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
|
{% 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 }}">
|
<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 %}
|
{{ widget.input_text }}:{% endif %}
|
||||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
|
<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 %}
|
{% 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")
|
@register.filter("money")
|
||||||
def money_filter(value: Decimal, arg='', hide_currency=False):
|
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)
|
value = Decimal(value)
|
||||||
if not isinstance(value, Decimal):
|
if not isinstance(value, Decimal):
|
||||||
if value == '':
|
if value == '':
|
||||||
@@ -47,7 +47,7 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
|
|||||||
|
|
||||||
@register.filter("money_numberfield")
|
@register.filter("money_numberfield")
|
||||||
def money_numberfield_filter(value: Decimal, arg=''):
|
def money_numberfield_filter(value: Decimal, arg=''):
|
||||||
if isinstance(value, float) or isinstance(value, int):
|
if isinstance(value, (float, int)):
|
||||||
value = Decimal(value)
|
value = Decimal(value)
|
||||||
if not isinstance(value, Decimal):
|
if not isinstance(value, Decimal):
|
||||||
raise TypeError("Invalid data type passed to money filter: %r" % type(value))
|
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):
|
except (AttributeError, IndexError):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if isinstance(message, str) or isinstance(message, Promise):
|
if isinstance(message, (str, Promise)):
|
||||||
exception_repr = str(message)
|
exception_repr = str(message)
|
||||||
context = {
|
context = {
|
||||||
'request_path': request.path,
|
'request_path': request.path,
|
||||||
|
|||||||
+129
-31
@@ -4,43 +4,25 @@ import celery.exceptions
|
|||||||
from celery.result import AsyncResult
|
from celery.result import AsyncResult
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
|
from django.test import RequestFactory
|
||||||
from django.utils.translation import gettext as _
|
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
|
from pretix.celery_app import app
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.base.tasks')
|
logger = logging.getLogger('pretix.base.tasks')
|
||||||
|
|
||||||
|
|
||||||
class AsyncAction:
|
class AsyncMixin:
|
||||||
task = None
|
|
||||||
success_url = None
|
success_url = None
|
||||||
error_url = None
|
error_url = None
|
||||||
known_errortypes = []
|
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):
|
def get_success_url(self, value):
|
||||||
return self.success_url
|
return self.success_url
|
||||||
|
|
||||||
@@ -50,11 +32,6 @@ class AsyncAction:
|
|||||||
def get_check_url(self, task_id, ajax):
|
def get_check_url(self, task_id, ajax):
|
||||||
return self.request.path + '?async_id=%s' % task_id + ('&ajax=1' if ajax else '')
|
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):
|
def _ajax_response_data(self):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -86,7 +63,7 @@ class AsyncAction:
|
|||||||
if smes:
|
if smes:
|
||||||
messages.success(self.request, smes)
|
messages.success(self.request, smes)
|
||||||
# TODO: Do not store message if the ajax client states that it will not redirect
|
# 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({
|
data.update({
|
||||||
'redirect': self.get_success_url(res.info),
|
'redirect': self.get_success_url(res.info),
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -95,7 +72,7 @@ class AsyncAction:
|
|||||||
else:
|
else:
|
||||||
messages.error(self.request, self.get_error_message(res.info))
|
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
|
# 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({
|
data.update({
|
||||||
'redirect': self.get_error_url(),
|
'redirect': self.get_error_url(),
|
||||||
'success': False,
|
'success': False,
|
||||||
@@ -159,3 +136,124 @@ class AsyncAction:
|
|||||||
|
|
||||||
def get_success_message(self, value):
|
def get_success_message(self, value):
|
||||||
return _('The task has been completed.')
|
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 import File
|
||||||
from django.core.files.uploadedfile import UploadedFile
|
from django.core.files.uploadedfile import UploadedFile
|
||||||
from django.forms.utils import from_current_timezone
|
from django.forms.utils import from_current_timezone
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -122,7 +123,7 @@ class CachedFileInput(forms.ClearableFileInput):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
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):
|
def value_from_datadict(self, data, files, name):
|
||||||
from ...base.models import CachedFile
|
from ...base.models import CachedFile
|
||||||
@@ -200,6 +201,8 @@ class CachedFileField(ExtFileField):
|
|||||||
from ...base.models import CachedFile
|
from ...base.models import CachedFile
|
||||||
|
|
||||||
if isinstance(data, File):
|
if isinstance(data, File):
|
||||||
|
if hasattr(data, '_uploaded_to'):
|
||||||
|
return data._uploaded_to
|
||||||
cf = CachedFile.objects.create(
|
cf = CachedFile.objects.create(
|
||||||
expires=now() + datetime.timedelta(days=1),
|
expires=now() + datetime.timedelta(days=1),
|
||||||
date=now(),
|
date=now(),
|
||||||
@@ -209,6 +212,7 @@ class CachedFileField(ExtFileField):
|
|||||||
)
|
)
|
||||||
cf.file.save(data.name, data.file)
|
cf.file.save(data.name, data.file)
|
||||||
cf.save()
|
cf.save()
|
||||||
|
data._uploaded_to = cf
|
||||||
return cf
|
return cf
|
||||||
return super().bound_data(data, initial)
|
return super().bound_data(data, initial)
|
||||||
|
|
||||||
@@ -217,6 +221,8 @@ class CachedFileField(ExtFileField):
|
|||||||
|
|
||||||
data = super().clean(*args, **kwargs)
|
data = super().clean(*args, **kwargs)
|
||||||
if isinstance(data, File):
|
if isinstance(data, File):
|
||||||
|
if hasattr(data, '_uploaded_to'):
|
||||||
|
return data._uploaded_to
|
||||||
cf = CachedFile.objects.create(
|
cf = CachedFile.objects.create(
|
||||||
expires=now() + datetime.timedelta(days=1),
|
expires=now() + datetime.timedelta(days=1),
|
||||||
web_download=True,
|
web_download=True,
|
||||||
@@ -226,6 +232,7 @@ class CachedFileField(ExtFileField):
|
|||||||
)
|
)
|
||||||
cf.file.save(data.name, data.file)
|
cf.file.save(data.name, data.file)
|
||||||
cf.save()
|
cf.save()
|
||||||
|
data._uploaded_to = cf
|
||||||
return cf
|
return cf
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|||||||
@@ -449,6 +449,11 @@ class EventSettingsForm(SettingsForm):
|
|||||||
'waiting_list_enabled',
|
'waiting_list_enabled',
|
||||||
'waiting_list_hours',
|
'waiting_list_hours',
|
||||||
'waiting_list_auto',
|
'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',
|
'max_items_per_order',
|
||||||
'reservation_time',
|
'reservation_time',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
@@ -459,6 +464,7 @@ class EventSettingsForm(SettingsForm):
|
|||||||
'frontpage_subevent_ordering',
|
'frontpage_subevent_ordering',
|
||||||
'event_list_type',
|
'event_list_type',
|
||||||
'frontpage_text',
|
'frontpage_text',
|
||||||
|
'event_info_text',
|
||||||
'attendee_names_asked',
|
'attendee_names_asked',
|
||||||
'attendee_names_required',
|
'attendee_names_required',
|
||||||
'attendee_emails_asked',
|
'attendee_emails_asked',
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ from urllib.parse import urlencode
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
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.db.models.functions import Coalesce, ExtractWeekDay
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.formats import date_format, localize
|
from django.utils.formats import date_format, localize
|
||||||
@@ -153,6 +155,7 @@ class OrderFilterForm(FilterForm):
|
|||||||
(Order.STATUS_CANCELED, _('Canceled (fully)')),
|
(Order.STATUS_CANCELED, _('Canceled (fully)')),
|
||||||
('cp', _('Canceled (fully or with paid fee)')),
|
('cp', _('Canceled (fully or with paid fee)')),
|
||||||
('rc', _('Cancellation requested')),
|
('rc', _('Cancellation requested')),
|
||||||
|
('cni', _('Fully canceled but invoice not canceled')),
|
||||||
)),
|
)),
|
||||||
(_('Payment process'), (
|
(_('Payment process'), (
|
||||||
(Order.STATUS_EXPIRED, _('Expired')),
|
(Order.STATUS_EXPIRED, _('Expired')),
|
||||||
@@ -264,6 +267,18 @@ class OrderFilterForm(FilterForm):
|
|||||||
status=Order.STATUS_PAID,
|
status=Order.STATUS_PAID,
|
||||||
pending_sum_t__gt=0
|
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':
|
elif s == 'pa':
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
status=Order.STATUS_PENDING,
|
status=Order.STATUS_PENDING,
|
||||||
@@ -1424,6 +1439,8 @@ class VoucherFilterForm(FilterForm):
|
|||||||
s = fdata.get('tag').strip()
|
s = fdata.get('tag').strip()
|
||||||
if s == '<>':
|
if s == '<>':
|
||||||
qs = qs.filter(Q(tag__isnull=True) | Q(tag=''))
|
qs = qs.filter(Q(tag__isnull=True) | Q(tag=''))
|
||||||
|
elif s[0] == '"' and s[-1] == '"':
|
||||||
|
qs = qs.filter(tag__iexact=s[1:-1])
|
||||||
else:
|
else:
|
||||||
qs = qs.filter(tag__icontains=s)
|
qs = qs.filter(tag__icontains=s)
|
||||||
|
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ class ItemCreateForm(I18nModelForm):
|
|||||||
setattr(self.instance, f, getattr(self.cleaned_data['copy_from'], f))
|
setattr(self.instance, f, getattr(self.cleaned_data['copy_from'], f))
|
||||||
else:
|
else:
|
||||||
# Add to all sales channels by default
|
# 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
|
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
|
||||||
instance = super().save(*args, **kwargs)
|
instance = super().save(*args, **kwargs)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class ExtendForm(I18nModelForm):
|
|||||||
return super().save(commit)
|
return super().save(commit)
|
||||||
|
|
||||||
|
|
||||||
class ConfirmPaymentForm(forms.Form):
|
class ForceQuotaConfirmationForm(forms.Form):
|
||||||
force = forms.BooleanField(
|
force = forms.BooleanField(
|
||||||
label=_('Overbook quota and ignore late payment'),
|
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 '
|
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']
|
del self.fields['force']
|
||||||
|
|
||||||
|
|
||||||
class CancelForm(ConfirmPaymentForm):
|
class ConfirmPaymentForm(ForceQuotaConfirmationForm):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReactivateOrderForm(ForceQuotaConfirmationForm):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CancelForm(ForceQuotaConfirmationForm):
|
||||||
send_email = forms.BooleanField(
|
send_email = forms.BooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Notify customer by email'),
|
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, '
|
'in your cancellation fee if you want to keep them. Please always enter a gross value, '
|
||||||
'tax will be calculated automatically.'),
|
'tax will be calculated automatically.'),
|
||||||
)
|
)
|
||||||
|
cancel_invoice = forms.BooleanField(
|
||||||
|
label=_('Generate cancellation for invoice'),
|
||||||
|
initial=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -130,6 +143,8 @@ class CancelForm(ConfirmPaymentForm):
|
|||||||
self.fields['cancellation_fee'].max_value = prs
|
self.fields['cancellation_fee'].max_value = prs
|
||||||
else:
|
else:
|
||||||
del self.fields['cancellation_fee']
|
del self.fields['cancellation_fee']
|
||||||
|
if not self.instance.invoices.exists():
|
||||||
|
del self.fields['cancel_invoice']
|
||||||
|
|
||||||
def clean_cancellation_fee(self):
|
def clean_cancellation_fee(self):
|
||||||
val = self.cleaned_data['cancellation_fee'] or Decimal('0.00')
|
val = self.cleaned_data['cancellation_fee'] or Decimal('0.00')
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from io import StringIO
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.validators import EmailValidator
|
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.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes.forms import SafeModelChoiceField
|
from django_scopes.forms import SafeModelChoiceField
|
||||||
@@ -346,8 +346,8 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
data = super().clean()
|
data = super().clean()
|
||||||
|
|
||||||
vouchers = self.instance.event.vouchers.annotate(
|
vouchers = self.instance.event.vouchers.annotate(
|
||||||
code_lower=Lower('code')
|
code_upper=Upper('code')
|
||||||
).filter(code_lower__in=[c.lower() for c in data['codes']])
|
).filter(code_upper__in=[c.upper() for c in data['codes']])
|
||||||
if vouchers.exists():
|
if vouchers.exists():
|
||||||
raise ValidationError(_('A voucher with one of these codes already exists.'))
|
raise ValidationError(_('A voucher with one of these codes already exists.'))
|
||||||
|
|
||||||
@@ -377,26 +377,5 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def save(self, event, *args, **kwargs):
|
def post_bulk_save(self, objs):
|
||||||
objs = []
|
pass
|
||||||
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
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
|
|||||||
|
|
||||||
|
|
||||||
def current_url(request):
|
def current_url(request):
|
||||||
if len(request.GET):
|
if request.GET:
|
||||||
return request.path + '?' + request.GET.urlencode()
|
return request.path + '?' + request.GET.urlencode()
|
||||||
else:
|
else:
|
||||||
return request.path
|
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
|
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.
|
``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.
|
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
{% if request.event.has_subevents %}
|
{% 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">
|
||||||
<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>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<p>
|
<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-select2-url="{% url "control:event.subevents.select2" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||||
data-placeholder="{% trans "All dates" context "subevent" %}">
|
data-placeholder="{% trans "All dates" context "subevent" %}">
|
||||||
{% for se in selected_subevents %}
|
{% for se in selected_subevents %}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
|
|
||||||
{% if request.event.has_subevents %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% if not request.event.has_subevents or subevent %}
|
{% if not request.event.has_subevents or subevent %}
|
||||||
|
|||||||
@@ -193,6 +193,7 @@
|
|||||||
{% bootstrap_field sform.checkout_phone_helptext layout="control" %}
|
{% bootstrap_field sform.checkout_phone_helptext layout="control" %}
|
||||||
{% bootstrap_field sform.banner_text layout="control" %}
|
{% bootstrap_field sform.banner_text layout="control" %}
|
||||||
{% bootstrap_field sform.banner_text_bottom layout="control" %}
|
{% bootstrap_field sform.banner_text_bottom layout="control" %}
|
||||||
|
{% bootstrap_field sform.event_info_text layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Shop design" %}</legend>
|
<legend>{% trans "Shop design" %}</legend>
|
||||||
@@ -248,6 +249,9 @@
|
|||||||
{% bootstrap_field sform.waiting_list_enabled layout="control" %}
|
{% bootstrap_field sform.waiting_list_enabled layout="control" %}
|
||||||
{% bootstrap_field sform.waiting_list_auto layout="control" %}
|
{% bootstrap_field sform.waiting_list_auto layout="control" %}
|
||||||
{% bootstrap_field sform.waiting_list_hours 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>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Item metadata" %}</legend>
|
<legend>{% trans "Item metadata" %}</legend>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{% if request.event.has_subevents %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% if quotas|length == 0 %}
|
{% if quotas|length == 0 %}
|
||||||
|
|||||||
@@ -23,13 +23,16 @@
|
|||||||
<input type="hidden" name="status" value="c"/>
|
<input type="hidden" name="status" value="c"/>
|
||||||
{% bootstrap_form_errors form %}
|
{% bootstrap_form_errors form %}
|
||||||
{% bootstrap_field form.send_email layout='' %}
|
{% bootstrap_field form.send_email layout='' %}
|
||||||
|
{% if form.cancel_invoice %}
|
||||||
|
{% bootstrap_field form.cancel_invoice layout='' %}
|
||||||
|
{% endif %}
|
||||||
{% if form.cancellation_fee %}
|
{% if form.cancellation_fee %}
|
||||||
{% bootstrap_field form.cancellation_fee layout='' %}
|
{% bootstrap_field form.cancellation_fee layout='' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="row checkout-button-row">
|
<div class="row checkout-button-row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<a class="btn btn-block btn-default btn-lg"
|
<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" %}
|
{% trans "No, take me back" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
<form method="post" class="form-horizontal" href="">
|
<form method="post" class="form-horizontal" href="">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form layout='horizontal' horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
|
||||||
<div class="form-group submit-group">
|
<div class="form-group submit-group">
|
||||||
<a class="btn btn-default btn-lg"
|
<a class="btn 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 %}">
|
||||||
|
|||||||
@@ -170,6 +170,10 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ o.total|money:request.event.currency }}
|
{{ 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>
|
||||||
<td class="text-right flip">{{ o.pcnt|default_if_none:"0" }}</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>
|
<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 title %}{% trans "Voucher" %}{% endblock %}
|
||||||
{% block inside %}
|
{% block inside %}
|
||||||
<h1>{% trans "Create multiple vouchers" %}</h1>
|
<h1>{% trans "Create multiple vouchers" %}</h1>
|
||||||
<form action="" method="post" class="form-horizontal">
|
<form action="" method="post" class="form-horizontal" data-asynctask>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% bootstrap_form_errors form %}
|
{% bootstrap_form_errors form %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<strong>
|
<strong>
|
||||||
{% if t.tag %}
|
{% 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 }}
|
{{ t.tag }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -48,15 +48,9 @@
|
|||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.event.has_subevents %}
|
{% if request.event.has_subevents %}
|
||||||
<select name="subevent" class="form-control">
|
<div class="col-md-6">
|
||||||
<option value="">{% trans "All dates" context "subevent" %}</option>
|
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||||
{% for se in request.event.subevents.all %}
|
</div>
|
||||||
<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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button class="btn btn-large btn-primary" type="submit">
|
<button class="btn btn-large btn-primary" type="submit">
|
||||||
{% trans "Send as many vouchers as possible" %}
|
{% trans "Send as many vouchers as possible" %}
|
||||||
@@ -80,59 +74,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>
|
<form class="row filter-form" action="" method="get">
|
||||||
<form class="form-inline helper-display-inline" action="" method="get">
|
<div class="col-lg-2 col-md-3 col-xs-6">
|
||||||
<select name="status" class="form-control">
|
<select name="status" class="form-control">
|
||||||
<option value="a"
|
<option value="a"
|
||||||
{% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "All entries" %}</option>
|
{% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "All entries" %}</option>
|
||||||
<option value="w"
|
<option value="w"
|
||||||
{% if request.GET.status == "w" or not request.GET.status %}selected="selected"{% endif %}>
|
{% if request.GET.status == "w" or not request.GET.status %}selected="selected"{% endif %}>
|
||||||
{% trans "Waiting for a voucher" %}</option>
|
{% trans "Waiting for a voucher" %}</option>
|
||||||
<option value="s"
|
<option value="s"
|
||||||
{% if request.GET.status == "s" %}selected="selected"{% endif %}>{% trans "Voucher assigned" %}</option>
|
{% if request.GET.status == "s" %}selected="selected"{% endif %}>{% trans "Voucher assigned" %}</option>
|
||||||
<option value="v"
|
<option value="v"
|
||||||
{% if request.GET.status == "v" %}selected="selected"{% endif %}>
|
{% if request.GET.status == "v" %}selected="selected"{% endif %}>
|
||||||
{% trans "Waiting for redemption" %}</option>
|
{% trans "Waiting for redemption" %}</option>
|
||||||
<option value="r"
|
<option value="r"
|
||||||
{% if request.GET.status == "r" %}selected="selected"{% endif %}>
|
{% if request.GET.status == "r" %}selected="selected"{% endif %}>
|
||||||
{% trans "Successfully redeemed" %}</option>
|
{% trans "Successfully redeemed" %}</option>
|
||||||
<option value="e"
|
<option value="e"
|
||||||
{% if request.GET.status == "e" %}selected="selected"{% endif %}>
|
{% if request.GET.status == "e" %}selected="selected"{% endif %}>
|
||||||
{% trans "Voucher expired" %}</option>
|
{% trans "Voucher expired" %}</option>
|
||||||
</select>
|
</select>
|
||||||
<select name="item" class="form-control">
|
</div>
|
||||||
<option value="">{% trans "All products" %}</option>
|
<div class="col-lg-2 col-md-3 col-xs-6">
|
||||||
{% for item in items %}
|
<select name="item" class="form-control">
|
||||||
<option value="{{ item.id }}"
|
<option value="">{% trans "All products" %}</option>
|
||||||
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
|
{% for item in items %}
|
||||||
{{ item }}
|
<option value="{{ item.id }}"
|
||||||
</option>
|
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
|
||||||
{% endfor %}
|
{{ item }}
|
||||||
</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 }}
|
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</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 %}
|
{% endif %}
|
||||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
|
||||||
<a href="?{% url_replace request "download" "yes" %}"
|
<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>
|
class="btn btn-default"><i class="fa fa-download"></i>
|
||||||
{% trans "Download list" %}</a>
|
{% trans "Download list" %}</a>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</p>
|
|
||||||
<form method="post" action="?next={{ request.get_full_path|urlencode }}">
|
<form method="post" action="?next={{ request.get_full_path|urlencode }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-condensed table-hover">
|
<table class="table table-condensed table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
<th>{% trans "Product" %}</th>
|
||||||
{% if request.event.has_subevents %}
|
{% if request.event.has_subevents %}
|
||||||
<th>{% trans "Date" context "subevent" %}</th>
|
<th>{% trans "Date" context "subevent" %}</th>
|
||||||
@@ -146,7 +144,13 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for e in entries %}
|
{% for e in entries %}
|
||||||
<tr>
|
<tr>
|
||||||
|
{% if request.event.settings.waiting_list_names_asked %}
|
||||||
|
<td>{{ e.name|default:"" }}</td>
|
||||||
|
{% endif %}
|
||||||
<td>{{ e.email }}</td>
|
<td>{{ e.email }}</td>
|
||||||
|
{% if request.event.settings.waiting_list_phones_asked %}
|
||||||
|
<td>{{ e.phone|default:"" }}</td>
|
||||||
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
{{ e.item }}
|
{{ e.item }}
|
||||||
{% if e.variation %}
|
{% if e.variation %}
|
||||||
@@ -154,7 +158,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% if request.event.has_subevents %}
|
{% 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 %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
{{ e.created|date:"SHORT_DATETIME_FORMAT" }}
|
{{ e.created|date:"SHORT_DATETIME_FORMAT" }}
|
||||||
|
|||||||
@@ -691,14 +691,14 @@ class MailSettingsRendererPreview(MailSettingsPreview):
|
|||||||
expires=now(), code="PREVIEW", total=119)
|
expires=now(), code="PREVIEW", total=119)
|
||||||
item = request.event.items.create(name=gettext("Sample product"), default_price=42.23,
|
item = request.event.items.create(name=gettext("Sample product"), default_price=42.23,
|
||||||
description=gettext("Sample product description"))
|
description=gettext("Sample product description"))
|
||||||
p = order.positions.create(item=item, attendee_name_parts={'_legacy': gettext("John Doe")},
|
order.positions.create(item=item, attendee_name_parts={'_legacy': gettext("John Doe")},
|
||||||
price=item.default_price)
|
price=item.default_price, subevent=request.event.subevents.last())
|
||||||
v = renderers[request.GET.get('renderer')].render(
|
v = renderers[request.GET.get('renderer')].render(
|
||||||
v,
|
v,
|
||||||
str(request.event.settings.mail_text_signature),
|
str(request.event.settings.mail_text_signature),
|
||||||
gettext('Your order: %(code)s') % {'code': order.code},
|
gettext('Your order: %(code)s') % {'code': order.code},
|
||||||
order,
|
order,
|
||||||
position=p
|
position=None
|
||||||
)
|
)
|
||||||
r = HttpResponse(v, content_type='text/html')
|
r = HttpResponse(v, content_type='text/html')
|
||||||
r._csp_ignore = True
|
r._csp_ignore = True
|
||||||
@@ -955,11 +955,10 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
|
|||||||
return reverse('control:index')
|
return reverse('control:index')
|
||||||
|
|
||||||
|
|
||||||
class EventLog(EventPermissionRequiredMixin, ListView):
|
class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||||
template_name = 'pretixcontrol/event/logs.html'
|
template_name = 'pretixcontrol/event/logs.html'
|
||||||
model = LogEntry
|
model = LogEntry
|
||||||
context_object_name = 'logs'
|
context_object_name = 'logs'
|
||||||
paginate_by = 20
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.request.event.logentry_set.all().select_related(
|
qs = self.request.event.logentry_set.all().select_related(
|
||||||
@@ -1363,7 +1362,7 @@ class QuickSetupView(FormView):
|
|||||||
tax_rule=tax_rule,
|
tax_rule=tax_rule,
|
||||||
admission=True,
|
admission=True,
|
||||||
position=i,
|
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))
|
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']:
|
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,
|
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
|
||||||
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
||||||
OrderRefundForm, OtherOperationsForm,
|
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
|
||||||
)
|
)
|
||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
from pretix.control.signals import order_search_forms
|
from pretix.control.signals import order_search_forms
|
||||||
@@ -151,6 +151,11 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
|
|||||||
s = OrderPosition.objects.filter(
|
s = OrderPosition.objects.filter(
|
||||||
order=OuterRef('pk')
|
order=OuterRef('pk')
|
||||||
).order_by().values('order').annotate(k=Count('id')).values('k')
|
).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 = {
|
annotated = {
|
||||||
o['pk']: o
|
o['pk']: o
|
||||||
for o in
|
for o in
|
||||||
@@ -158,10 +163,11 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
|
|||||||
pk__in=[o.pk for o in ctx['orders']]
|
pk__in=[o.pk for o in ctx['orders']]
|
||||||
).annotate(
|
).annotate(
|
||||||
pcnt=Subquery(s, output_field=IntegerField()),
|
pcnt=Subquery(s, output_field=IntegerField()),
|
||||||
|
icnt=Subquery(i, output_field=IntegerField()),
|
||||||
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk')))
|
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk')))
|
||||||
).values(
|
).values(
|
||||||
'pk', 'pcnt', 'is_overpaid', 'is_underpaid', 'is_pending_with_full_payment', 'has_external_refund',
|
'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_pending_refund = annotated.get(o.pk)['has_pending_refund']
|
||||||
o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request']
|
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.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]
|
o.sales_channel_obj = scs[o.sales_channel]
|
||||||
|
|
||||||
if ctx['page_obj'].paginator.count < 1000:
|
if ctx['page_obj'].paginator.count < 1000:
|
||||||
@@ -1134,6 +1141,7 @@ class OrderTransition(OrderView):
|
|||||||
try:
|
try:
|
||||||
cancel_order(self.order.pk, user=self.request.user,
|
cancel_order(self.order.pk, user=self.request.user,
|
||||||
send_mail=self.mark_canceled_form.cleaned_data['send_email'],
|
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'))
|
cancellation_fee=self.mark_canceled_form.cleaned_data.get('cancellation_fee'))
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
messages.error(self.request, str(e))
|
messages.error(self.request, str(e))
|
||||||
@@ -1416,11 +1424,24 @@ class OrderExtend(OrderView):
|
|||||||
class OrderReactivate(OrderView):
|
class OrderReactivate(OrderView):
|
||||||
permission = 'can_change_orders'
|
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):
|
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:
|
try:
|
||||||
reactivate_order(
|
reactivate_order(
|
||||||
self.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.'))
|
messages.success(self.request, _('The order has been reactivated.'))
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
@@ -1445,6 +1466,7 @@ class OrderReactivate(OrderView):
|
|||||||
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
return render(self.request, 'pretixcontrol/order/reactivate.html', {
|
return render(self.request, 'pretixcontrol/order/reactivate.html', {
|
||||||
|
'form': self.reactivate_form,
|
||||||
'order': self.order,
|
'order': self.order,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -938,6 +938,7 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
|
|||||||
template_name = 'pretixcontrol/organizers/giftcards.html'
|
template_name = 'pretixcontrol/organizers/giftcards.html'
|
||||||
permission = 'can_manage_gift_cards'
|
permission = 'can_manage_gift_cards'
|
||||||
context_object_name = 'giftcards'
|
context_object_name = 'giftcards'
|
||||||
|
paginate_by = 50
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
s = GiftCardTransaction.objects.filter(
|
s = GiftCardTransaction.objects.filter(
|
||||||
@@ -1437,12 +1438,11 @@ class EventMetaPropertyDeleteView(OrganizerDetailViewMixin, OrganizerPermissionR
|
|||||||
return redirect(success_url)
|
return redirect(success_url)
|
||||||
|
|
||||||
|
|
||||||
class LogView(OrganizerPermissionRequiredMixin, ListView):
|
class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
||||||
template_name = 'pretixcontrol/organizers/logs.html'
|
template_name = 'pretixcontrol/organizers/logs.html'
|
||||||
permission = 'can_change_organizer_settings'
|
permission = 'can_change_organizer_settings'
|
||||||
model = LogEntry
|
model = LogEntry
|
||||||
context_object_name = 'logs'
|
context_object_name = 'logs'
|
||||||
paginate_by = 20
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.request.organizer.all_logentries().select_related(
|
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'):
|
for fname in ('size', 'name', 'release_after_exit'):
|
||||||
setattr(q, fname, f.cleaned_data.get(fname))
|
setattr(q, fname, f.cleaned_data.get(fname))
|
||||||
q.save(clear_cache=False)
|
q.save(clear_cache=False)
|
||||||
if 'itemvar' in f.changed_data:
|
if 'itemvars' in f.changed_data:
|
||||||
q.items.set(selected_items)
|
q.items.set(selected_items)
|
||||||
q.variations.set(selected_variations)
|
q.variations.set(selected_variations)
|
||||||
log_entries.append(
|
log_entries.append(
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ def subevent_select2(request, **kwargs):
|
|||||||
|
|
||||||
qs = request.event.subevents.filter(
|
qs = request.event.subevents.filter(
|
||||||
qf
|
qf
|
||||||
).order_by('-date_from')
|
).order_by('-date_from', 'name', 'pk')
|
||||||
|
|
||||||
total = qs.count()
|
total = qs.count()
|
||||||
pagesize = 20
|
pagesize = 20
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import io
|
|||||||
from defusedcsv import csv
|
from defusedcsv import csv
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
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.db.models import Sum
|
||||||
from django.http import (
|
from django.http import (
|
||||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
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 import CartPosition, LogEntry, OrderPosition, Voucher
|
||||||
from pretix.base.models.vouchers import _generate_random_code
|
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.services.vouchers import vouchers_send
|
||||||
|
from pretix.base.views.tasks import AsyncFormView
|
||||||
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
|
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
|
||||||
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
|
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
|
||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
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)
|
return redirect('control:event.vouchers', event=request.event.slug, organizer=request.event.organizer.slug)
|
||||||
|
|
||||||
|
|
||||||
class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
|
||||||
model = Voucher
|
model = Voucher
|
||||||
template_name = 'pretixcontrol/vouchers/bulk.html'
|
template_name = 'pretixcontrol/vouchers/bulk.html'
|
||||||
permission = 'can_change_vouchers'
|
permission = 'can_change_vouchers'
|
||||||
context_object_name = 'voucher'
|
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={
|
return reverse('control:event.vouchers', kwargs={
|
||||||
'organizer': self.request.event.organizer.slug,
|
'organizer': self.request.event.organizer.slug,
|
||||||
'event': self.request.event.slug,
|
'event': self.request.event.slug,
|
||||||
@@ -316,34 +325,84 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
|||||||
i.redeemed = 0
|
i.redeemed = 0
|
||||||
kwargs['instance'] = i
|
kwargs['instance'] = i
|
||||||
else:
|
else:
|
||||||
kwargs['instance'] = Voucher(event=self.request.event)
|
kwargs['instance'] = Voucher(event=self.request.event, code=None)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@transaction.atomic
|
def get_async_form_kwargs(self, form_kwargs, organizer=None, event=None):
|
||||||
def form_valid(self, form):
|
if not form_kwargs.get('instance'):
|
||||||
log_entries = []
|
form_kwargs['instance'] = Voucher(event=self.request.event, code=None)
|
||||||
objs = form.save(self.request.event)
|
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 = []
|
voucherids = []
|
||||||
for v in objs:
|
with lockfn(), transaction.atomic():
|
||||||
log_entries.append(
|
if not form.is_valid():
|
||||||
v.log_action('pretix.voucher.added', data=form.cleaned_data, user=self.request.user, save=False)
|
raise ValidationError(form.errors)
|
||||||
)
|
total_num = len(form.cleaned_data['codes'])
|
||||||
voucherids.append(v.pk)
|
|
||||||
LogEntry.objects.bulk_create(log_entries, batch_size=200)
|
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']:
|
if form.cleaned_data['send']:
|
||||||
vouchers_send.apply_async(kwargs={
|
vouchers_send(
|
||||||
'event': self.request.event.pk,
|
event=self.request.event,
|
||||||
'vouchers': voucherids,
|
vouchers=voucherids,
|
||||||
'subject': form.cleaned_data['send_subject'],
|
subject=form.cleaned_data['send_subject'],
|
||||||
'message': form.cleaned_data['send_message'],
|
message=form.cleaned_data['send_message'],
|
||||||
'recipients': [r._asdict() for r in form.cleaned_data['send_recipients']],
|
recipients=[r._asdict() for r in form.cleaned_data['send_recipients']],
|
||||||
'user': self.request.user.pk,
|
user=self.request.user.pk,
|
||||||
})
|
progress=lambda p: set_progress(50. + p * 50.)
|
||||||
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.'))
|
def get_success_message(self, value):
|
||||||
return HttpResponseRedirect(self.get_success_url())
|
return _('The new vouchers have been created.')
|
||||||
|
|
||||||
def get_form_class(self):
|
def get_form_class(self):
|
||||||
form_class = VoucherBulkForm
|
form_class = VoucherBulkForm
|
||||||
@@ -357,11 +416,6 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
|||||||
ctx['code_length'] = settings.ENTROPY['voucher_code']
|
ctx['code_length'] = settings.ENTROPY['voucher_code']
|
||||||
return ctx
|
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):
|
class VoucherRNG(EventPermissionRequiredMixin, View):
|
||||||
permission = 'can_change_vouchers'
|
permission = 'can_change_vouchers'
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
|
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC, delimiter=",")
|
||||||
|
|
||||||
headers = [
|
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')
|
_('Language'), _('Priority')
|
||||||
]
|
]
|
||||||
if self.request.event.has_subevents:
|
if self.request.event.has_subevents:
|
||||||
@@ -235,7 +235,9 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
status = _('Waiting')
|
status = _('Waiting')
|
||||||
|
|
||||||
row = [
|
row = [
|
||||||
|
w.name,
|
||||||
w.email,
|
w.email,
|
||||||
|
w.phone,
|
||||||
prod,
|
prod,
|
||||||
w.created.isoformat(),
|
w.created.isoformat(),
|
||||||
status,
|
status,
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ def merge(*args):
|
|||||||
"""Implements the 'merge' operator for merging lists."""
|
"""Implements the 'merge' operator for merging lists."""
|
||||||
ret = []
|
ret = []
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if isinstance(arg, list) or isinstance(arg, tuple):
|
if isinstance(arg, (list, tuple)):
|
||||||
ret += list(arg)
|
ret += list(arg)
|
||||||
else:
|
else:
|
||||||
ret.append(arg)
|
ret.append(arg)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ class Thumbnail(models.Model):
|
|||||||
unique_together = (('source', 'size'),)
|
unique_together = (('source', 'size'),)
|
||||||
|
|
||||||
|
|
||||||
def modelcopy(obj: models.Model):
|
def modelcopy(obj: models.Model, **kwargs):
|
||||||
n = obj.__class__()
|
n = obj.__class__(**kwargs)
|
||||||
for f in obj._meta.fields:
|
for f in obj._meta.fields:
|
||||||
val = getattr(obj, f.name)
|
val = getattr(obj, f.name)
|
||||||
if isinstance(val, models.Model):
|
if isinstance(val, models.Model):
|
||||||
|
|||||||
+1353
-1204
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2020-07-30 19:00+0000\n"
|
||||||
"Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n"
|
"Last-Translator: Abdullah <abdullah.gumaijan@gmail.com>\n"
|
||||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
"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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2020-12-14 10:00+0000\n"
|
||||||
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
|
"Last-Translator: Ondřej Sokol <osokol@treesoft.cz>\n"
|
||||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2020-09-15 02:00+0000\n"
|
||||||
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
|
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
|
||||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2020-08-25 02:00+0000\n"
|
||||||
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
|
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
|
||||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2020-08-25 02:00+0000\n"
|
||||||
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
|
"Last-Translator: Dennis Lichtenthäler <lichtenthaeler@rami.io>\n"
|
||||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
"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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
|
||||||
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
||||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2020-04-27 20:00+0000\n"
|
||||||
"Last-Translator: Gonzalo Gabriel Perez <zalitoar@gmail.com>\n"
|
"Last-Translator: Gonzalo Gabriel Perez <zalitoar@gmail.com>\n"
|
||||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
"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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2021-01-20 16:10+0000\n"
|
||||||
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
||||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
|
"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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: French\n"
|
"Project-Id-Version: French\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2020-09-15 17:00+0000\n"
|
||||||
"Last-Translator: Martin Gross <martin@pc-coholic.de>\n"
|
"Last-Translator: Martin Gross <martin@pc-coholic.de>\n"
|
||||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2020-01-24 08:00+0000\n"
|
||||||
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
|
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
|
||||||
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
|
"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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2020-06-12 20:00+0000\n"
|
||||||
"Last-Translator: Frank <webappconcept@gmail.com>\n"
|
"Last-Translator: Frank <webappconcept@gmail.com>\n"
|
||||||
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
|
"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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2019-11-13 06:00+0000\n"
|
||||||
"Last-Translator: Zane Smite <z.smite@riga-jurmala.com>\n"
|
"Last-Translator: Zane Smite <z.smite@riga-jurmala.com>\n"
|
||||||
"Language-Team: Latvian <https://translate.pretix.eu/projects/pretix/pretix-"
|
"Language-Team: Latvian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
|||||||
+1418
-1365
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user