forked from CGM_Public/pretix_original
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d220cce0f | |||
| 17574e8a23 | |||
| de7314edcc | |||
| df25a1cebf | |||
| 0578955273 | |||
| 0b4daa9b16 | |||
| 8dfc77a927 | |||
| d0f603283b | |||
| 3df61b8fb5 | |||
| fdead71884 | |||
| 369251b0b0 | |||
| e83798a9b7 | |||
| 4c9640561c | |||
| e9ab0d8654 | |||
| c9e5cce7d0 | |||
| 859004ec59 | |||
| 136511f394 | |||
| ee4081d9c3 | |||
| 28b4982161 | |||
| 5c8a3f18f3 | |||
| 440d1b5766 | |||
| 1ff8c6f78b | |||
| 71e6a85c38 | |||
| 497e6f5c8f | |||
| 1d60827fa1 | |||
| 3bbed98844 | |||
| 8b8ad34d30 | |||
| e64034ed90 | |||
| b185dce17c | |||
| 2ffa2315ca | |||
| b0616ed00d | |||
| c36b9bcfcd | |||
| 2533ae2b3a | |||
| 61ae434ab1 | |||
| 2ebbe82baf | |||
| 153eb67300 | |||
| a2ed32be8b | |||
| b5c94fd002 | |||
| 1b02a898a1 | |||
| f29aa73f8d | |||
| 62cbed4891 | |||
| 68e31b92fe | |||
| 9a90444cca | |||
| 18f57ea012 | |||
| 08a85b3dab | |||
| 81a5e263cb | |||
| 926d334b10 | |||
| 9bed40fa09 | |||
| ed1dae5fde | |||
| c7060d188f | |||
| 9a53dc9c5e | |||
| d1e0a7293b | |||
| 7d5b1eebcb | |||
| e4a02c494e | |||
| 6ecac70727 | |||
| 8a1554323e | |||
| cfc22c806a | |||
| f70d6877dc | |||
| 4dfad2ef42 | |||
| dc1d35cb1f | |||
| 6e657db882 | |||
| 91d3aaf20b | |||
| 2f1318e2b9 | |||
| 844a74d33f | |||
| 76788a874a | |||
| e29d5c37cd | |||
| 258e66587e | |||
| d13af2eeab | |||
| 14b9afcc40 |
@@ -137,6 +137,7 @@ Endpoints
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:query is_public: If set to ``true``/``false``, only events with a matching value of ``is_public`` are returned.
|
||||
:query live: If set to ``true``/``false``, only events with a matching value of ``live`` are returned.
|
||||
:query testmode: If set to ``true``/``false``, only events with a matching value of ``testmode`` are returned.
|
||||
:query has_subevents: If set to ``true``/``false``, only events with a matching value of ``has_subevents`` are returned.
|
||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. Event series are never (always) returned.
|
||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. Event series are never (always) returned.
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "4.17.1"
|
||||
__version__ = "4.18.0.dev0"
|
||||
|
||||
@@ -693,6 +693,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'frontpage_subevent_ordering',
|
||||
'event_list_type',
|
||||
'event_list_available_only',
|
||||
'event_calendar_future_only',
|
||||
'frontpage_text',
|
||||
'event_info_text',
|
||||
'attendee_names_asked',
|
||||
@@ -784,6 +785,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'change_allow_user_addons',
|
||||
'change_allow_user_until',
|
||||
'change_allow_user_price',
|
||||
'change_allow_attendee',
|
||||
'primary_color',
|
||||
'theme_color_success',
|
||||
'theme_color_danger',
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django.conf import settings
|
||||
from django.core.validators import URLValidator
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from rest_framework.exceptions import ValidationError
|
||||
@@ -69,3 +70,17 @@ class I18nAwareModelSerializer(ModelSerializer):
|
||||
|
||||
I18nAwareModelSerializer.serializer_field_mapping[I18nCharField] = I18nField
|
||||
I18nAwareModelSerializer.serializer_field_mapping[I18nTextField] = I18nField
|
||||
|
||||
|
||||
class I18nURLField(I18nField):
|
||||
def to_internal_value(self, value):
|
||||
value = super().to_internal_value(value)
|
||||
if not value:
|
||||
return value
|
||||
if isinstance(value.data, dict):
|
||||
for v in value.data.values():
|
||||
if v:
|
||||
URLValidator()(v)
|
||||
else:
|
||||
URLValidator()(value.data)
|
||||
return value
|
||||
|
||||
@@ -52,7 +52,7 @@ from pretix.base.models import (
|
||||
|
||||
|
||||
class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10,
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||
coerce_to_string=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
|
||||
@@ -76,7 +76,7 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10,
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||
coerce_to_string=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
|
||||
@@ -304,9 +304,9 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
if not self.instance:
|
||||
for addon_data in value:
|
||||
ItemAddOn.clean_categories(self.context['event'], None, self.instance, addon_data['addon_category'])
|
||||
ItemAddOn.clean_min_count(addon_data['min_count'])
|
||||
ItemAddOn.clean_max_count(addon_data['max_count'])
|
||||
ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count'])
|
||||
ItemAddOn.clean_min_count(addon_data.get('min_count', 0))
|
||||
ItemAddOn.clean_max_count(addon_data.get('max_count', 0))
|
||||
ItemAddOn.clean_max_min_count(addon_data.get('max_count', 0), addon_data.get('min_count', 0))
|
||||
return value
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -381,11 +381,9 @@ class PdfDataSerializer(serializers.Field):
|
||||
res['meta:' + k] = v
|
||||
|
||||
if instance.variation_id:
|
||||
print(instance, instance.variation, instance.variation_id, instance.item)
|
||||
if not hasattr(instance.variation, '_cached_meta_data'):
|
||||
instance.variation.item = instance.item # saves some database lookups
|
||||
instance.variation._cached_meta_data = instance.variation.meta_data
|
||||
print(instance.variation._cached_meta_data.items())
|
||||
for k, v in instance.variation._cached_meta_data.items():
|
||||
res['itemmeta:' + k] = v
|
||||
else:
|
||||
@@ -781,7 +779,7 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2,
|
||||
max_digits=10)
|
||||
max_digits=13)
|
||||
voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(),
|
||||
required=False, allow_null=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
@@ -47,7 +47,7 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
||||
attendee_name = serializers.CharField(required=False, allow_null=True)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2,
|
||||
max_digits=10)
|
||||
max_digits=13)
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -128,7 +128,7 @@ class MembershipSerializer(I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
value = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=Decimal('0.00'))
|
||||
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -72,7 +72,7 @@ with scopes_disabled():
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ['is_public', 'live', 'has_subevents']
|
||||
fields = ['is_public', 'live', 'has_subevents', 'testmode']
|
||||
|
||||
def ends_after_qs(self, queryset, name, value):
|
||||
expr = (
|
||||
|
||||
@@ -112,6 +112,7 @@ class AuthorizationView(RecentAuthenticationRequiredMixin, BaseAuthorizationView
|
||||
self.request.user.log_action('pretix.user.oauth.authorized', user=self.request.user, data={
|
||||
'application_id': application.pk,
|
||||
'application_name': application.name,
|
||||
'organizers': [o.pk for o in form.cleaned_data.get("organizers")] if form.cleaned_data.get("organizers") else []
|
||||
})
|
||||
|
||||
return self.redirect(self.success_url, application)
|
||||
|
||||
@@ -105,9 +105,9 @@ with scopes_disabled():
|
||||
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
|
||||
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
item = django_filters.CharFilter(field_name='all_positions', lookup_expr='item_id')
|
||||
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id')
|
||||
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id')
|
||||
item = django_filters.CharFilter(field_name='all_positions', lookup_expr='item_id', distinct=True)
|
||||
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
|
||||
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -1507,7 +1507,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
@action(detail=True, methods=['POST'])
|
||||
def refund(self, request, **kwargs):
|
||||
payment = self.get_object()
|
||||
amount = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
|
||||
amount = serializers.DecimalField(max_digits=13, decimal_places=2).to_internal_value(
|
||||
request.data.get('amount', str(payment.amount))
|
||||
)
|
||||
if 'mark_refunded' in request.data:
|
||||
|
||||
@@ -197,7 +197,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
@transaction.atomic()
|
||||
def transact(self, request, **kwargs):
|
||||
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
|
||||
value = serializers.DecimalField(max_digits=10, decimal_places=2).to_internal_value(
|
||||
value = serializers.DecimalField(max_digits=13, decimal_places=2).to_internal_value(
|
||||
request.data.get('value')
|
||||
)
|
||||
text = serializers.CharField(allow_blank=True, allow_null=True).to_internal_value(
|
||||
|
||||
@@ -646,6 +646,10 @@ def base_placeholders(sender, **kwargs):
|
||||
'attendee_name', ['position'], lambda position: position.attendee_name,
|
||||
_('John Doe'),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'positionid', ['position'], lambda position: str(position.positionid),
|
||||
'1'
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'name', ['position_or_address'],
|
||||
get_best_name,
|
||||
|
||||
@@ -218,7 +218,9 @@ class ItemDataExporter(ListExporter):
|
||||
yield row
|
||||
|
||||
def get_filename(self):
|
||||
return '{}_products'.format(self.events.first().organizer.slug)
|
||||
if self.is_multievent:
|
||||
return '{}_products'.format(self.events.first().organizer.slug)
|
||||
return '{}_products'.format(self.event.slug)
|
||||
|
||||
def prepare_xlsx_sheet(self, ws):
|
||||
self.__ws = ws
|
||||
|
||||
@@ -36,11 +36,13 @@ import logging
|
||||
|
||||
import i18nfield.forms
|
||||
from django import forms
|
||||
from django.core.validators import URLValidator
|
||||
from django.forms.models import ModelFormMetaclass
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
from hierarkey.forms import HierarkeyForm
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
|
||||
@@ -222,3 +224,17 @@ class SecretKeySettingsField(forms.CharField):
|
||||
if value == SECRET_REDACTED:
|
||||
return
|
||||
return super().run_validators(value)
|
||||
|
||||
|
||||
class I18nURLFormField(i18nfield.forms.I18nFormField):
|
||||
def clean(self, value) -> LazyI18nString:
|
||||
value = super().clean(value)
|
||||
if not value:
|
||||
return value
|
||||
if isinstance(value.data, dict):
|
||||
for v in value.data.values():
|
||||
if v:
|
||||
URLValidator()(v)
|
||||
else:
|
||||
URLValidator()(value.data)
|
||||
return value
|
||||
|
||||
@@ -719,7 +719,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
label = escape(q.question) # django-bootstrap3 calls mark_safe
|
||||
required = q.required and not self.all_optional
|
||||
if q.type == Question.TYPE_BOOLEAN:
|
||||
if q.required:
|
||||
if required:
|
||||
# For some reason, django-bootstrap3 does not set the required attribute
|
||||
# itself.
|
||||
widget = forms.CheckboxInput(attrs={'required': 'required'})
|
||||
|
||||
@@ -148,6 +148,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
"""
|
||||
stylesheet = StyleSheet1()
|
||||
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12))
|
||||
stylesheet.add(ParagraphStyle(name='NormalRight', fontName=self.font_regular, fontSize=10, leading=12, alignment=TA_RIGHT))
|
||||
stylesheet.add(ParagraphStyle(name='BoldInverseCenter', fontName=self.font_bold, fontSize=10, leading=12,
|
||||
textColor=colors.white, alignment=TA_CENTER))
|
||||
stylesheet.add(ParagraphStyle(name='InvoiceFrom', parent=stylesheet['Normal']))
|
||||
@@ -610,8 +611,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
),
|
||||
str(len(lines)),
|
||||
localize(tax_rate) + " %",
|
||||
money_filter(net_value * len(lines), self.invoice.event.currency),
|
||||
money_filter(gross_value * len(lines), self.invoice.event.currency),
|
||||
Paragraph(money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), self.stylesheet['NormalRight']),
|
||||
Paragraph(money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), self.stylesheet['NormalRight']),
|
||||
))
|
||||
else:
|
||||
if len(lines) > 1:
|
||||
@@ -625,7 +626,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
self.stylesheet['Normal']
|
||||
),
|
||||
str(len(lines)),
|
||||
money_filter(gross_value * len(lines), self.invoice.event.currency),
|
||||
Paragraph(money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), self.stylesheet['NormalRight']),
|
||||
))
|
||||
taxvalue_map[tax_rate, tax_name] += (gross_value - net_value) * len(lines)
|
||||
grossvalue_map[tax_rate, tax_name] += gross_value * len(lines)
|
||||
@@ -633,12 +634,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if has_taxes:
|
||||
tdata.append([
|
||||
pgettext('invoice', 'Invoice total'), '', '', '', money_filter(total, self.invoice.event.currency)
|
||||
pgettext('invoice', 'Invoice total'), '', '', '',
|
||||
money_filter(total, self.invoice.event.currency)
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
|
||||
else:
|
||||
tdata.append([
|
||||
pgettext('invoice', 'Invoice total'), '', money_filter(total, self.invoice.event.currency)
|
||||
pgettext('invoice', 'Invoice total'), '',
|
||||
money_filter(total, self.invoice.event.currency)
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.65, .20, .15)]
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ class Command(BaseCommand):
|
||||
OrderPosition.objects.filter(
|
||||
order=OuterRef('pk')
|
||||
).order_by().values('order').annotate(p=Sum('price')).values('p'),
|
||||
output_field=models.DecimalField(decimal_places=2, max_digits=10)
|
||||
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
|
||||
output_field=models.DecimalField(decimal_places=2, max_digits=13)
|
||||
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=13)
|
||||
),
|
||||
position_cnt=Case(
|
||||
When(Q(status__in=('e', 'c')) | Q(require_approval=True), then=Value(0)),
|
||||
@@ -64,16 +64,16 @@ class Command(BaseCommand):
|
||||
OrderFee.objects.filter(
|
||||
order=OuterRef('pk')
|
||||
).order_by().values('order').annotate(p=Sum('value')).values('p'),
|
||||
output_field=models.DecimalField(decimal_places=2, max_digits=10)
|
||||
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
|
||||
output_field=models.DecimalField(decimal_places=2, max_digits=13)
|
||||
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=13)
|
||||
),
|
||||
tx_total=Coalesce(
|
||||
Subquery(
|
||||
Transaction.objects.filter(
|
||||
order=OuterRef('pk')
|
||||
).order_by().values('order').annotate(p=Sum(F('price') * F('count'))).values('p'),
|
||||
output_field=models.DecimalField(decimal_places=2, max_digits=10)
|
||||
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
|
||||
output_field=models.DecimalField(decimal_places=2, max_digits=13)
|
||||
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=13)
|
||||
),
|
||||
tx_cnt=Coalesce(
|
||||
Subquery(
|
||||
@@ -81,15 +81,15 @@ class Command(BaseCommand):
|
||||
order=OuterRef('pk'),
|
||||
item__isnull=False,
|
||||
).order_by().values('order').annotate(p=Sum(F('count'))).values('p'),
|
||||
output_field=models.DecimalField(decimal_places=2, max_digits=10)
|
||||
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
|
||||
output_field=models.DecimalField(decimal_places=2, max_digits=13)
|
||||
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=13)
|
||||
),
|
||||
).annotate(
|
||||
correct_total=Case(
|
||||
When(Q(status=Order.STATUS_CANCELED) | Q(status=Order.STATUS_EXPIRED) | Q(require_approval=True),
|
||||
then=Value(0)),
|
||||
default=F('position_total') + F('fee_total'),
|
||||
output_field=models.DecimalField(decimal_places=2, max_digits=10)
|
||||
output_field=models.DecimalField(decimal_places=2, max_digits=13)
|
||||
),
|
||||
).exclude(
|
||||
total=F('position_total') + F('fee_total'),
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
# Generated by Django 3.2.18 on 2023-03-16 20:23
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0234_total_ordering'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='cancellationrequest',
|
||||
name='cancellation_fee',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='custom_price_input',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='line_price_gross',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='listed_price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='price_after_voucher',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='discount',
|
||||
name='condition_min_value',
|
||||
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='giftcardtransaction',
|
||||
name='value',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='foreign_currency_rate',
|
||||
field=models.DecimalField(decimal_places=4, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoiceline',
|
||||
name='gross_value',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoiceline',
|
||||
name='tax_value',
|
||||
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
name='default_price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
name='original_price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='itembundle',
|
||||
name='designated_price',
|
||||
field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='itemvariation',
|
||||
name='default_price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='itemvariation',
|
||||
name='original_price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='total',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderfee',
|
||||
name='tax_value',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderfee',
|
||||
name='value',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderpayment',
|
||||
name='amount',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='tax_value',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='voucher_budget_use',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderrefund',
|
||||
name='amount',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subeventitem',
|
||||
name='price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subeventitemvariation',
|
||||
name='price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transaction',
|
||||
name='price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transaction',
|
||||
name='tax_value',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='budget',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='value',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=13, null=True),
|
||||
),
|
||||
]
|
||||
@@ -55,7 +55,12 @@ class CheckinList(LoggedModel):
|
||||
all_products = models.BooleanField(default=True, verbose_name=_("All products (including newly created ones)"))
|
||||
limit_products = models.ManyToManyField('Item', verbose_name=_("Limit to products"), blank=True)
|
||||
subevent = models.ForeignKey('SubEvent', null=True, blank=True,
|
||||
verbose_name=pgettext_lazy('subevent', 'Date'), on_delete=models.CASCADE)
|
||||
verbose_name=pgettext_lazy('subevent', 'Date'),
|
||||
on_delete=models.CASCADE,
|
||||
help_text=_('If you choose "all dates", tickets will be considered part of this list '
|
||||
'and valid for check-in regardless of which date they are purchased for. '
|
||||
'You can limit their validity through the advanced check-in rules, '
|
||||
'though.'))
|
||||
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
|
||||
default=False,
|
||||
help_text=_('With this option, people will be able to check in even if the '
|
||||
|
||||
@@ -116,7 +116,7 @@ class Discount(LoggedModel):
|
||||
condition_min_value = models.DecimalField(
|
||||
verbose_name=_('Minimum gross value of matching products'),
|
||||
decimal_places=2,
|
||||
max_digits=10,
|
||||
max_digits=13,
|
||||
default=Decimal('0.00'),
|
||||
)
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ class GiftCardTransaction(models.Model):
|
||||
)
|
||||
value = models.DecimalField(
|
||||
decimal_places=2,
|
||||
max_digits=10
|
||||
max_digits=13
|
||||
)
|
||||
order = models.ForeignKey(
|
||||
'Order',
|
||||
|
||||
@@ -152,7 +152,7 @@ class Invoice(models.Model):
|
||||
footer_text = models.TextField(blank=True)
|
||||
|
||||
foreign_currency_display = models.CharField(max_length=50, null=True, blank=True)
|
||||
foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=10, null=True, blank=True)
|
||||
foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=13, null=True, blank=True)
|
||||
foreign_currency_rate_date = models.DateField(null=True, blank=True)
|
||||
foreign_currency_source = models.CharField(max_length=100, null=True, blank=True)
|
||||
|
||||
@@ -347,8 +347,8 @@ class InvoiceLine(models.Model):
|
||||
invoice = models.ForeignKey('Invoice', related_name='lines', on_delete=models.CASCADE)
|
||||
position = models.PositiveIntegerField(default=0)
|
||||
description = models.TextField()
|
||||
gross_value = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
tax_value = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
||||
gross_value = models.DecimalField(max_digits=13, decimal_places=2)
|
||||
tax_value = models.DecimalField(max_digits=13, decimal_places=2, default=Decimal('0.00'))
|
||||
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
|
||||
tax_name = models.CharField(max_length=190)
|
||||
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
|
||||
|
||||
@@ -164,7 +164,7 @@ class SubEventItem(models.Model):
|
||||
"""
|
||||
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
|
||||
item = models.ForeignKey('Item', on_delete=models.CASCADE)
|
||||
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||
price = models.DecimalField(max_digits=13, decimal_places=2, null=True, blank=True)
|
||||
disabled = models.BooleanField(default=False, verbose_name=_('Disable product for this date'))
|
||||
available_from = models.DateTimeField(
|
||||
verbose_name=_("Available from"),
|
||||
@@ -220,7 +220,7 @@ class SubEventItemVariation(models.Model):
|
||||
"""
|
||||
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
|
||||
variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE)
|
||||
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||
price = models.DecimalField(max_digits=13, decimal_places=2, null=True, blank=True)
|
||||
disabled = models.BooleanField(default=False, verbose_name=_('Disable product for this date'))
|
||||
available_from = models.DateTimeField(
|
||||
verbose_name=_("Available from"),
|
||||
@@ -407,7 +407,7 @@ class Item(LoggedModel):
|
||||
help_text=_("If this product has multiple variations, you can set different prices for each of the "
|
||||
"variations. If a variation does not have a special price or if you do not have variations, "
|
||||
"this price will be used."),
|
||||
max_digits=7, decimal_places=2, null=True
|
||||
max_digits=13, decimal_places=2, null=True
|
||||
)
|
||||
free_price = models.BooleanField(
|
||||
default=False,
|
||||
@@ -538,7 +538,7 @@ class Item(LoggedModel):
|
||||
original_price = models.DecimalField(
|
||||
verbose_name=_('Original price'),
|
||||
blank=True, null=True,
|
||||
max_digits=7, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
|
||||
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
|
||||
)
|
||||
@@ -952,14 +952,14 @@ class ItemVariation(models.Model):
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
default_price = models.DecimalField(
|
||||
decimal_places=2, max_digits=7,
|
||||
decimal_places=2, max_digits=13,
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Default price"),
|
||||
)
|
||||
original_price = models.DecimalField(
|
||||
verbose_name=_('Original price'),
|
||||
blank=True, null=True,
|
||||
max_digits=7, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
|
||||
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
|
||||
)
|
||||
@@ -1304,7 +1304,7 @@ class ItemBundle(models.Model):
|
||||
)
|
||||
designated_price = models.DecimalField(
|
||||
default=Decimal('0.00'), blank=True,
|
||||
decimal_places=2, max_digits=10,
|
||||
decimal_places=2, max_digits=13,
|
||||
verbose_name=_('Designated price part'),
|
||||
help_text=_('If set, it will be shown that this bundled item is responsible for the given value of the total '
|
||||
'gross price. This might be important in cases of mixed taxation, but can be kept blank otherwise. This '
|
||||
|
||||
@@ -220,7 +220,7 @@ class Order(LockModel, LoggedModel):
|
||||
verbose_name=_("Expiration date")
|
||||
)
|
||||
total = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
decimal_places=2, max_digits=13,
|
||||
verbose_name=_("Total amount")
|
||||
)
|
||||
comment = models.TextField(
|
||||
@@ -403,8 +403,8 @@ class Order(LockModel, LoggedModel):
|
||||
state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT),
|
||||
order=OuterRef('pk')
|
||||
)
|
||||
payment_sum_sq = Subquery(payment_sum, output_field=models.DecimalField(decimal_places=2, max_digits=10))
|
||||
refund_sum_sq = Subquery(refund_sum, output_field=models.DecimalField(decimal_places=2, max_digits=10))
|
||||
payment_sum_sq = Subquery(payment_sum, output_field=models.DecimalField(decimal_places=2, max_digits=13))
|
||||
refund_sum_sq = Subquery(refund_sum, output_field=models.DecimalField(decimal_places=2, max_digits=13))
|
||||
if sums:
|
||||
qs = qs.annotate(
|
||||
payment_sum=payment_sum_sq,
|
||||
@@ -626,7 +626,10 @@ class Order(LockModel, LoggedModel):
|
||||
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
|
||||
).select_related('item').prefetch_related('issued_gift_cards')
|
||||
)
|
||||
cancelable = all([op.item.allow_cancel and not op.has_checkin for op in positions])
|
||||
if self.event.settings.change_allow_user_if_checked_in:
|
||||
cancelable = all([op.item.allow_cancel for op in positions])
|
||||
else:
|
||||
cancelable = all([op.item.allow_cancel and not op.has_checkin for op in positions])
|
||||
if not cancelable or not positions:
|
||||
return False
|
||||
for op in positions:
|
||||
@@ -985,7 +988,7 @@ class Order(LockModel, LoggedModel):
|
||||
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
|
||||
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
|
||||
auth=None, attach_tickets=False, position: 'OrderPosition'=None, auto_email=True,
|
||||
attach_ical=False, attach_other_files: list=None):
|
||||
attach_ical=False, attach_other_files: list=None, attach_cached_files: list=None):
|
||||
"""
|
||||
Sends an email to the user that placed this order. Basically, this method does two things:
|
||||
|
||||
@@ -1030,7 +1033,7 @@ class Order(LockModel, LoggedModel):
|
||||
self.event, self.locale, self, headers=headers, sender=sender,
|
||||
invoices=invoices, attach_tickets=attach_tickets,
|
||||
position=position, auto_email=auto_email, attach_ical=attach_ical,
|
||||
attach_other_files=attach_other_files,
|
||||
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
|
||||
)
|
||||
except SendMailException:
|
||||
raise
|
||||
@@ -1313,7 +1316,7 @@ class AbstractPosition(models.Model):
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
price = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
decimal_places=2, max_digits=13,
|
||||
verbose_name=_("Price")
|
||||
)
|
||||
attendee_name_cached = models.CharField(
|
||||
@@ -1545,7 +1548,7 @@ class OrderPayment(models.Model):
|
||||
max_length=190, choices=PAYMENT_STATES
|
||||
)
|
||||
amount = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
decimal_places=2, max_digits=13,
|
||||
verbose_name=_("Amount")
|
||||
)
|
||||
order = models.ForeignKey(
|
||||
@@ -1929,7 +1932,7 @@ class OrderRefund(models.Model):
|
||||
max_length=190, choices=REFUND_SOURCES
|
||||
)
|
||||
amount = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
decimal_places=2, max_digits=13,
|
||||
verbose_name=_("Amount")
|
||||
)
|
||||
order = models.ForeignKey(
|
||||
@@ -2078,7 +2081,7 @@ class OrderFee(models.Model):
|
||||
)
|
||||
|
||||
value = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
decimal_places=2, max_digits=13,
|
||||
verbose_name=_("Value")
|
||||
)
|
||||
order = models.ForeignKey(
|
||||
@@ -2102,7 +2105,7 @@ class OrderFee(models.Model):
|
||||
null=True, blank=True
|
||||
)
|
||||
tax_value = models.DecimalField(
|
||||
max_digits=10, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
)
|
||||
canceled = models.BooleanField(default=False)
|
||||
@@ -2236,7 +2239,7 @@ class OrderPosition(AbstractPosition):
|
||||
)
|
||||
|
||||
voucher_budget_use = models.DecimalField(
|
||||
max_digits=10, decimal_places=2, null=True, blank=True,
|
||||
max_digits=13, decimal_places=2, null=True, blank=True,
|
||||
)
|
||||
|
||||
tax_rate = models.DecimalField(
|
||||
@@ -2249,7 +2252,7 @@ class OrderPosition(AbstractPosition):
|
||||
null=True, blank=True
|
||||
)
|
||||
tax_value = models.DecimalField(
|
||||
max_digits=10, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
)
|
||||
|
||||
@@ -2555,6 +2558,27 @@ class OrderPosition(AbstractPosition):
|
||||
attach_tickets=True
|
||||
)
|
||||
|
||||
@property
|
||||
@scopes_disabled()
|
||||
def attendee_change_allowed(self) -> bool:
|
||||
"""
|
||||
Returns whether or not this order can be changed by the attendee.
|
||||
"""
|
||||
from .items import ItemAddOn
|
||||
|
||||
if not self.event.settings.change_allow_attendee or not self.order.user_change_allowed:
|
||||
return False
|
||||
|
||||
positions = list(
|
||||
self.order.positions.filter(Q(pk=self.pk) | Q(addon_to_id=self.pk)).annotate(
|
||||
has_variations=Exists(ItemVariation.objects.filter(item_id=OuterRef('item_id'))),
|
||||
).select_related('item').prefetch_related('issued_gift_cards')
|
||||
)
|
||||
return (
|
||||
(self.order.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])) or
|
||||
(self.order.event.settings.change_allow_user_addons and ItemAddOn.objects.filter(base_item_id__in=[op.item_id for op in positions]).exists())
|
||||
)
|
||||
|
||||
|
||||
class Transaction(models.Model):
|
||||
"""
|
||||
@@ -2649,7 +2673,7 @@ class Transaction(models.Model):
|
||||
verbose_name=pgettext_lazy("subevent", "Date"),
|
||||
)
|
||||
price = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
decimal_places=2, max_digits=13,
|
||||
verbose_name=_("Price")
|
||||
)
|
||||
tax_rate = models.DecimalField(
|
||||
@@ -2662,7 +2686,7 @@ class Transaction(models.Model):
|
||||
null=True, blank=True
|
||||
)
|
||||
tax_value = models.DecimalField(
|
||||
max_digits=10, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
)
|
||||
fee_type = models.CharField(
|
||||
@@ -2740,19 +2764,19 @@ class CartPosition(AbstractPosition):
|
||||
verbose_name=_('Tax rate')
|
||||
)
|
||||
listed_price = models.DecimalField(
|
||||
decimal_places=2, max_digits=10, null=True,
|
||||
decimal_places=2, max_digits=13, null=True,
|
||||
)
|
||||
price_after_voucher = models.DecimalField(
|
||||
decimal_places=2, max_digits=10, null=True,
|
||||
decimal_places=2, max_digits=13, null=True,
|
||||
)
|
||||
custom_price_input = models.DecimalField(
|
||||
decimal_places=2, max_digits=10, null=True,
|
||||
decimal_places=2, max_digits=13, null=True,
|
||||
)
|
||||
custom_price_input_is_net = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
line_price_gross = models.DecimalField(
|
||||
decimal_places=2, max_digits=10, null=True,
|
||||
decimal_places=2, max_digits=13, null=True,
|
||||
)
|
||||
requested_valid_from = models.DateTimeField(
|
||||
null=True,
|
||||
@@ -2822,7 +2846,7 @@ class CartPosition(AbstractPosition):
|
||||
# Migrate from pre-discounts position
|
||||
if self.item.free_price and self.custom_price_input is None:
|
||||
custom_price = self.price
|
||||
if custom_price > 100000000:
|
||||
if custom_price > 99_999_999_999:
|
||||
raise ValueError('price_too_high')
|
||||
self.custom_price_input = custom_price
|
||||
self.custom_price_input_is_net = not False
|
||||
@@ -3035,7 +3059,7 @@ class CachedCombinedTicket(models.Model):
|
||||
class CancellationRequest(models.Model):
|
||||
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='cancellation_requests')
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
cancellation_fee = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
cancellation_fee = models.DecimalField(max_digits=13, decimal_places=2)
|
||||
refund_as_giftcard = models.BooleanField(default=False)
|
||||
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ class Voucher(LoggedModel):
|
||||
verbose_name=_("Maximum discount budget"),
|
||||
help_text=_("This is the maximum monetary amount that will be discounted using this voucher across all usages. "
|
||||
"If this is sum reached, the voucher can no longer be used."),
|
||||
decimal_places=2, max_digits=10,
|
||||
decimal_places=2, max_digits=13,
|
||||
null=True, blank=True
|
||||
)
|
||||
valid_until = models.DateTimeField(
|
||||
@@ -243,7 +243,7 @@ class Voucher(LoggedModel):
|
||||
)
|
||||
value = models.DecimalField(
|
||||
verbose_name=_("Voucher value"),
|
||||
decimal_places=2, max_digits=10, null=True, blank=True,
|
||||
decimal_places=2, max_digits=13, null=True, blank=True,
|
||||
)
|
||||
item = models.ForeignKey(
|
||||
Item, related_name='vouchers',
|
||||
@@ -599,7 +599,7 @@ class Voucher(LoggedModel):
|
||||
Order.STATUS_PENDING
|
||||
]
|
||||
).order_by().values('voucher_id').annotate(s=Sum('voucher_budget_use')).values('s')
|
||||
return qs.annotate(budget_used_orders=Coalesce(Subquery(opq, output_field=models.DecimalField(max_digits=10, decimal_places=2)), Decimal('0.00')))
|
||||
return qs.annotate(budget_used_orders=Coalesce(Subquery(opq, output_field=models.DecimalField(max_digits=13, decimal_places=2)), Decimal('0.00')))
|
||||
|
||||
def budget_used(self):
|
||||
ops = OrderPosition.objects.filter(
|
||||
|
||||
@@ -188,6 +188,9 @@ class WaitingListEntry(LoggedModel):
|
||||
raise WaitingListException(_('This entry is anonymized and can no longer be used.'))
|
||||
|
||||
with transaction.atomic():
|
||||
e = self.email
|
||||
if self.name:
|
||||
e += ' / ' + self.name
|
||||
v = Voucher.objects.create(
|
||||
event=self.event,
|
||||
max_usages=1,
|
||||
@@ -196,7 +199,7 @@ class WaitingListEntry(LoggedModel):
|
||||
variation=self.variation,
|
||||
tag='waiting-list',
|
||||
comment=_('Automatically created from waiting list entry for {email}').format(
|
||||
email=self.email
|
||||
email=e
|
||||
),
|
||||
block_quota=True,
|
||||
subevent=self.subevent,
|
||||
|
||||
@@ -1125,6 +1125,12 @@ class ManualPayment(BasePaymentProvider):
|
||||
widget=I18nTextarea,
|
||||
validators=[PlaceholderValidator(['{order}', '{amount}', '{currency}', '{amount_with_currency}'])],
|
||||
)),
|
||||
('invoice_immediately',
|
||||
forms.BooleanField(
|
||||
label=_('Create an invoice for orders using bank transfer immediately if the event is otherwise '
|
||||
'configured to create invoices after payment is completed.'),
|
||||
required=False,
|
||||
)),
|
||||
] + list(super().settings_form_fields.items())
|
||||
)
|
||||
d.move_to_end('_enabled', last=False)
|
||||
@@ -1164,6 +1170,10 @@ class ManualPayment(BasePaymentProvider):
|
||||
format_map(self.settings.get('pending_description', as_type=LazyI18nString), self.format_map(payment.order, payment))
|
||||
)
|
||||
|
||||
@property
|
||||
def requires_invoice_immediately(self):
|
||||
return self.settings.get('invoice_immediately', False, as_type=bool)
|
||||
|
||||
|
||||
class OffsettingProvider(BasePaymentProvider):
|
||||
is_enabled = True
|
||||
|
||||
@@ -777,16 +777,16 @@ class Renderer:
|
||||
qr_y = float(o['bottom']) * mm
|
||||
renderPDF.draw(d, canvas, qr_x, qr_y)
|
||||
|
||||
# Add QR content + PDF issuer as a hidden string (fully transparent & off page)
|
||||
# Add QR content + PDF issuer as a hidden string (fully transparent & very very small)
|
||||
# This helps automated processing of the PDF file by 3rd parties, e.g. when checking tickets for resale
|
||||
data = {
|
||||
"issuer": settings.SITE_URL,
|
||||
o.get('content', 'secret'): content
|
||||
}
|
||||
canvas.saveState()
|
||||
canvas.setFont('Open Sans', 1)
|
||||
canvas.setFont('Open Sans', .01)
|
||||
canvas.setFillColorRGB(0, 0, 0, 0)
|
||||
canvas.drawString(-1000 * mm, -1000 * mm, json.dumps(data, sort_keys=True))
|
||||
canvas.drawString(0 * mm, 0 * mm, json.dumps(data, sort_keys=True))
|
||||
canvas.restoreState()
|
||||
|
||||
def _get_ev(self, op, order):
|
||||
|
||||
@@ -726,7 +726,7 @@ class CartManager:
|
||||
custom_price = None
|
||||
if item.free_price and i.get('price'):
|
||||
custom_price = Decimal(str(i.get('price')).replace(",", "."))
|
||||
if custom_price > 100000000:
|
||||
if custom_price > 99_999_999_999:
|
||||
raise ValueError('price_too_high')
|
||||
|
||||
op = self.AddOperation(
|
||||
@@ -841,7 +841,7 @@ class CartManager:
|
||||
custom_price = None
|
||||
if item.free_price and a.get('price'):
|
||||
custom_price = Decimal(str(a.get('price')).replace(",", "."))
|
||||
if custom_price > 100000000:
|
||||
if custom_price > 99_999_999_999:
|
||||
raise ValueError('price_too_high')
|
||||
|
||||
# Fix positions with wrong price (TODO: happens out-of-cartmanager-transaction and therefore a little hacky)
|
||||
@@ -1141,7 +1141,7 @@ class CartManager:
|
||||
custom_price_input=op.custom_price_input,
|
||||
custom_price_input_is_net=op.custom_price_input_is_net,
|
||||
line_price_gross=line_price.gross,
|
||||
tax_rate=line_price.tax,
|
||||
tax_rate=line_price.rate,
|
||||
price=line_price.gross,
|
||||
)
|
||||
if self.event.settings.attendee_names_asked:
|
||||
@@ -1188,7 +1188,7 @@ class CartManager:
|
||||
custom_price_input=b.custom_price_input,
|
||||
custom_price_input_is_net=b.custom_price_input_is_net,
|
||||
line_price_gross=bline_price.gross,
|
||||
tax_rate=bline_price.tax,
|
||||
tax_rate=bline_price.rate,
|
||||
price=bline_price.gross,
|
||||
is_bundled=True
|
||||
))
|
||||
|
||||
@@ -47,7 +47,8 @@ from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, Sum, Value,
|
||||
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Sum,
|
||||
Value,
|
||||
)
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.db.transaction import get_connection
|
||||
@@ -188,6 +189,7 @@ error_messages = {
|
||||
'min'
|
||||
),
|
||||
'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
|
||||
'addon_already_checked_in': gettext_lazy('You cannot remove the position %(addon)s since it has already been checked in.'),
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1607,6 +1609,7 @@ class OrderChangeManager:
|
||||
override_tax_rate=new_rate)
|
||||
self._totaldiff += new_tax.gross - pos.price
|
||||
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
|
||||
self._invoice_dirty = True
|
||||
|
||||
def cancel_fee(self, fee: OrderFee):
|
||||
self._totaldiff -= fee.value
|
||||
@@ -1701,7 +1704,19 @@ class OrderChangeManager:
|
||||
for a in position.addons.all():
|
||||
self._operations.append(self.SplitOperation(a))
|
||||
|
||||
def set_addons(self, addons):
|
||||
def set_addons(self, addons, limit_main_positions=None):
|
||||
"""
|
||||
This is a convenience method to change the add-on products selected on an order. The input structure is similar
|
||||
to CartManager.set_addons. It will automatically compute the correct operations to add, cancel, or change
|
||||
positions on the order. Every existing add-on not in the input will be canceled. Availability of the
|
||||
products is validated (with some exceptions).
|
||||
|
||||
:param addons: A list of dictionaries with the keys ``"addon_to"``, ``"item"``, ``"variation"`` (all ID values),
|
||||
``"count"``, and ``"price"``.
|
||||
:param limit_main_positions: By default, the method works on all methods of the order. If you set this to a
|
||||
queryset or a list of positions, all other positions and their add-ons will be kept
|
||||
untouched.
|
||||
"""
|
||||
if self._operations:
|
||||
raise ValueError("Setting addons should be the first/only operation")
|
||||
|
||||
@@ -1713,7 +1728,13 @@ class OrderChangeManager:
|
||||
quota_diff = Counter() # Quota -> Number of usages
|
||||
available_categories = defaultdict(set) # OrderPos -> Category IDs to choose from
|
||||
price_included = defaultdict(dict) # OrderPos -> CategoryID -> bool(price is included)
|
||||
toplevel_op = self.order.positions.filter(
|
||||
if isinstance(limit_main_positions, QuerySet):
|
||||
toplevel_qs = limit_main_positions
|
||||
elif limit_main_positions is not None:
|
||||
toplevel_qs = self.order.positions.filter(pk__in=[p.pk for p in limit_main_positions])
|
||||
else:
|
||||
toplevel_qs = self.order.positions
|
||||
toplevel_op = toplevel_qs.filter(
|
||||
addon_to__isnull=True
|
||||
).prefetch_related(
|
||||
'addons', 'item__addons', 'item__addons__addon_category'
|
||||
@@ -1877,6 +1898,12 @@ class OrderChangeManager:
|
||||
for a in current_addons[cp][k][:current_num - input_num]:
|
||||
if a.canceled:
|
||||
continue
|
||||
if a.checkins.exists():
|
||||
raise OrderError(
|
||||
error_messages['addon_already_checked_in'] % {
|
||||
'addon': str(a.item.name),
|
||||
}
|
||||
)
|
||||
self.cancel(a)
|
||||
|
||||
def _check_seats(self):
|
||||
@@ -2516,7 +2543,7 @@ class OrderChangeManager:
|
||||
positions_to_fake_cart[op.position].seat = op.seat
|
||||
elif isinstance(op, self.MembershipOperation):
|
||||
positions_to_fake_cart[op.position].used_membership = op.membership
|
||||
elif isinstance(op, self.CancelOperation):
|
||||
elif isinstance(op, self.CancelOperation) and op.position in positions_to_fake_cart:
|
||||
fake_cart.remove(positions_to_fake_cart[op.position])
|
||||
elif isinstance(op, self.AddOperation):
|
||||
cp = CartPosition(
|
||||
|
||||
@@ -70,7 +70,7 @@ def get_price(item: Item, variation: ItemVariation = None,
|
||||
elif item.free_price and custom_price is not None and custom_price != "":
|
||||
if not isinstance(custom_price, Decimal):
|
||||
custom_price = Decimal(str(custom_price).replace(",", "."))
|
||||
if custom_price > 100000000:
|
||||
if custom_price > 99_999_999_999:
|
||||
raise ValueError('price_too_high')
|
||||
|
||||
price = tax_rule.tax(price, invoice_address=invoice_address)
|
||||
|
||||
@@ -112,7 +112,7 @@ def dictsum(*dicts) -> dict:
|
||||
|
||||
def order_overview(
|
||||
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False,
|
||||
admission_only=False, base_qs=None
|
||||
admission_only=False, base_qs=None, base_fees_qs=None,
|
||||
) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
|
||||
items = event.items.all().select_related(
|
||||
'category', # for re-grouping
|
||||
@@ -233,7 +233,8 @@ def order_overview(
|
||||
payment_items = []
|
||||
|
||||
if subevent is None and fees:
|
||||
qs = OrderFee.all.filter(
|
||||
qs = OrderFee.all if base_fees_qs is None else base_fees_qs
|
||||
qs = qs.filter(
|
||||
order__event=event
|
||||
).annotate(
|
||||
status=Case(
|
||||
|
||||
+52
-11
@@ -63,7 +63,8 @@ from rest_framework import serializers
|
||||
from pretix.api.serializers.fields import (
|
||||
ListMultipleChoiceField, UploadedFileField,
|
||||
)
|
||||
from pretix.api.serializers.i18n import I18nField
|
||||
from pretix.api.serializers.i18n import I18nField, I18nURLField
|
||||
from pretix.base.forms import I18nURLFormField
|
||||
from pretix.base.models.tax import VAT_ID_COUNTRIES, TaxRule
|
||||
from pretix.base.reldate import (
|
||||
RelativeDateField, RelativeDateTimeField, RelativeDateWrapper,
|
||||
@@ -1396,6 +1397,19 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Hide all unavailable dates from calendar or list views"),
|
||||
help_text=_("This option currently only affects the calendar of this event series, not the organizer-wide "
|
||||
"calendar.")
|
||||
)
|
||||
},
|
||||
'event_calendar_future_only': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Hide all past dates from calendar"),
|
||||
help_text=_("This option currently only affects the calendar of this event series, not the organizer-wide "
|
||||
"calendar.")
|
||||
)
|
||||
},
|
||||
'allow_modifications_after_checkin': {
|
||||
@@ -1470,6 +1484,32 @@ DEFAULTS = {
|
||||
label=_("Do not allow changes after"),
|
||||
)
|
||||
},
|
||||
'change_allow_user_if_checked_in': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Allow change even though the ticket has already been checked in"),
|
||||
help_text=_("By default, order changes are disabled after any ticket in the order has been checked in. "
|
||||
"If you check this box, this requirement is lifted. It is still not possible to remove an "
|
||||
"add-on product that has already been checked in individually. Use with care, and preferably "
|
||||
"only in combination with a limitation on price changes above."),
|
||||
)
|
||||
},
|
||||
'change_allow_attendee': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Allow individual attendees to change their ticket"),
|
||||
help_text=_("By default, only the person who ordered the tickets can make any changes. If you check this "
|
||||
"box, individual attendees can also make changes. However, individual attendees can always "
|
||||
"only make changes that do not change the total price of the order. Such changes can always "
|
||||
"only be made by the main customer."),
|
||||
)
|
||||
},
|
||||
'cancel_allow_user': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
@@ -1485,7 +1525,7 @@ DEFAULTS = {
|
||||
'form_class': forms.DecimalField,
|
||||
'serializer_class': serializers.DecimalField,
|
||||
'serializer_kwargs': dict(
|
||||
max_digits=10, decimal_places=2
|
||||
max_digits=13, decimal_places=2
|
||||
),
|
||||
'form_kwargs': dict(
|
||||
label=_("Charge a fixed cancellation fee"),
|
||||
@@ -1510,7 +1550,7 @@ DEFAULTS = {
|
||||
'form_class': forms.DecimalField,
|
||||
'serializer_class': serializers.DecimalField,
|
||||
'serializer_kwargs': dict(
|
||||
max_digits=10, decimal_places=2
|
||||
max_digits=13, decimal_places=2
|
||||
),
|
||||
'form_kwargs': dict(
|
||||
label=_("Charge a percentual cancellation fee"),
|
||||
@@ -1544,7 +1584,7 @@ DEFAULTS = {
|
||||
'form_class': forms.DecimalField,
|
||||
'serializer_class': serializers.DecimalField,
|
||||
'serializer_kwargs': dict(
|
||||
max_digits=10, decimal_places=2
|
||||
max_digits=13, decimal_places=2
|
||||
),
|
||||
'form_kwargs': dict(
|
||||
label=_("Keep a fixed cancellation fee"),
|
||||
@@ -1565,7 +1605,7 @@ DEFAULTS = {
|
||||
'form_class': forms.DecimalField,
|
||||
'serializer_class': serializers.DecimalField,
|
||||
'serializer_kwargs': dict(
|
||||
max_digits=10, decimal_places=2
|
||||
max_digits=13, decimal_places=2
|
||||
),
|
||||
'form_kwargs': dict(
|
||||
label=_("Keep a percentual cancellation fee"),
|
||||
@@ -1604,10 +1644,10 @@ DEFAULTS = {
|
||||
'form_class': forms.DecimalField,
|
||||
'serializer_class': serializers.DecimalField,
|
||||
'serializer_kwargs': dict(
|
||||
max_digits=10, decimal_places=2
|
||||
max_digits=13, decimal_places=2
|
||||
),
|
||||
'form_kwargs': dict(
|
||||
max_digits=10, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
label=_("Step size for reduction amount"),
|
||||
help_text=_('By default, customers can choose an arbitrary amount for you to keep. If you set this to e.g. '
|
||||
'10, they will only be able to choose values in increments of 10.')
|
||||
@@ -1689,14 +1729,15 @@ DEFAULTS = {
|
||||
},
|
||||
'privacy_url': {
|
||||
'default': None,
|
||||
'type': str,
|
||||
'form_class': forms.URLField,
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nURLFormField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Privacy Policy URL"),
|
||||
help_text=_("This should point e.g. to a part of your website that explains how you use data gathered in "
|
||||
"your ticket shop."),
|
||||
widget=I18nTextInput,
|
||||
),
|
||||
'serializer_class': serializers.URLField,
|
||||
'serializer_class': I18nURLField,
|
||||
},
|
||||
'confirm_texts': {
|
||||
'default': LazyI18nStringList(),
|
||||
@@ -3243,7 +3284,7 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = {
|
||||
'CA': (['Province', 'Territory'], 'short'),
|
||||
# 'CN': (['Province', 'Autonomous region', 'Munincipality'], 'long'),
|
||||
'MY': (['State'], 'long'),
|
||||
'MX': (['State', 'Federal District'], 'short'),
|
||||
'MX': (['State', 'Federal district'], 'short'),
|
||||
'US': (['State', 'Outlying area', 'District'], 'short'),
|
||||
}
|
||||
|
||||
|
||||
@@ -151,16 +151,19 @@ class BaseDataShredder:
|
||||
|
||||
def shred_log_fields(logentry, banlist=None, whitelist=None):
|
||||
d = logentry.parsed_data
|
||||
shredded = False
|
||||
if whitelist:
|
||||
for k, v in d.items():
|
||||
if k not in whitelist:
|
||||
d[k] = '█'
|
||||
shredded = True
|
||||
elif banlist:
|
||||
for f in banlist:
|
||||
if f in d:
|
||||
d[f] = '█'
|
||||
shredded = True
|
||||
logentry.data = json.dumps(d)
|
||||
logentry.shredded = True
|
||||
logentry.shredded = logentry.shredded or shredded
|
||||
logentry.save(update_fields=['data', 'shredded'])
|
||||
|
||||
|
||||
@@ -380,9 +383,10 @@ class QuestionAnswerShredder(BaseDataShredder):
|
||||
d = le.parsed_data
|
||||
if 'data' in d:
|
||||
for i, row in enumerate(d['data']):
|
||||
for f in row:
|
||||
if f not in ('attendee_name', 'attendee_email'):
|
||||
d['data'][i][f] = '█'
|
||||
if isinstance(d['data'], list):
|
||||
for f in row:
|
||||
if f not in ('attendee_name', 'attendee_email'):
|
||||
d['data'][i][f] = '█'
|
||||
le.data = json.dumps(d)
|
||||
le.shredded = True
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
|
||||
@@ -511,6 +511,7 @@ class EventSettingsForm(SettingsForm):
|
||||
'low_availability_percentage',
|
||||
'event_list_type',
|
||||
'event_list_available_only',
|
||||
'event_calendar_future_only',
|
||||
'frontpage_text',
|
||||
'event_info_text',
|
||||
'attendee_names_asked',
|
||||
@@ -602,6 +603,7 @@ class EventSettingsForm(SettingsForm):
|
||||
del self.fields['frontpage_subevent_ordering']
|
||||
del self.fields['event_list_type']
|
||||
del self.fields['event_list_available_only']
|
||||
del self.fields['event_calendar_future_only']
|
||||
|
||||
# create "virtual" fields for better UX when editing <name>_asked and <name>_required fields
|
||||
self.virtual_keys = []
|
||||
@@ -688,6 +690,8 @@ class CancelSettingsForm(SettingsForm):
|
||||
'change_allow_user_price',
|
||||
'change_allow_user_until',
|
||||
'change_allow_user_addons',
|
||||
'change_allow_user_if_checked_in',
|
||||
'change_allow_attendee',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -1213,6 +1217,7 @@ class MailSettingsForm(SettingsForm):
|
||||
'mail_subject_order_canceled': ['event', 'order', 'comment'],
|
||||
'mail_text_order_expire_warning': ['event', 'order'],
|
||||
'mail_subject_order_expire_warning': ['event', 'order'],
|
||||
'mail_text_order_pending_warning': ['event', 'order'],
|
||||
'mail_subject_order_pending_warning': ['event', 'order'],
|
||||
'mail_text_order_incomplete_payment': ['event', 'order', 'pending_sum'],
|
||||
'mail_subject_order_incomplete_payment': ['event', 'order'],
|
||||
@@ -1374,7 +1379,10 @@ class TaxRuleLineForm(I18nForm):
|
||||
invoice_text = I18nFormField(
|
||||
label=_('Text on invoice'),
|
||||
required=False,
|
||||
widget=I18nTextInput
|
||||
widget=I18nTextInput,
|
||||
widget_kwargs=dict(attrs={
|
||||
'placeholder': _('Text on invoice'),
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -1564,7 +1572,7 @@ class QuickSetupProductForm(I18nForm):
|
||||
)
|
||||
default_price = forms.DecimalField(
|
||||
label=_("Price (optional)"),
|
||||
max_digits=7, decimal_places=2, required=False,
|
||||
max_digits=13, decimal_places=2, required=False,
|
||||
localize=True,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
|
||||
@@ -61,7 +61,7 @@ from pretix.base.models import (
|
||||
SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
|
||||
)
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.control.forms.widgets import Select2
|
||||
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
||||
from pretix.control.signals import order_search_filter_q
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
from pretix.helpers.database import (
|
||||
@@ -383,8 +383,6 @@ class OrderFilterForm(FilterForm):
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(*get_deterministic_ordering(Order, self.get_order_by()))
|
||||
else:
|
||||
qs = qs.order_by('-datetime', '-pk')
|
||||
|
||||
if fdata.get('provider'):
|
||||
qs = qs.annotate(
|
||||
@@ -2204,7 +2202,30 @@ class CheckinFilterForm(FilterForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['device'].queryset = self.event.organizer.devices.all().order_by('device_id')
|
||||
self.fields['device'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:organizer.devices.select2', kwargs={
|
||||
'organizer': self.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('All devices'),
|
||||
}
|
||||
)
|
||||
self.fields['device'].widget.choices = self.fields['device'].choices
|
||||
self.fields['device'].label = _('Device')
|
||||
|
||||
self.fields['gate'].queryset = self.event.organizer.gates.all()
|
||||
self.fields['gate'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:organizer.gates.select2', kwargs={
|
||||
'organizer': self.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('All gates'),
|
||||
}
|
||||
)
|
||||
self.fields['gate'].widget.choices = self.fields['gate'].choices
|
||||
self.fields['gate'].label = _('Gate')
|
||||
|
||||
self.fields['checkin_list'].queryset = self.event.checkin_lists.all()
|
||||
self.fields['checkin_list'].widget = Select2(
|
||||
@@ -2231,6 +2252,20 @@ class CheckinFilterForm(FilterForm):
|
||||
choices.append((str(i.pk), str(i)))
|
||||
self.fields['itemvar'].choices = choices
|
||||
|
||||
self.fields['itemvar'].choices = choices
|
||||
self.fields['itemvar'].widget = Select2ItemVarQuota(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:event.items.itemvar.select2', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('All products')
|
||||
}
|
||||
)
|
||||
self.fields['itemvar'].required = False
|
||||
self.fields['itemvar'].widget.choices = self.fields['itemvar'].choices
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ class CancelForm(ForceQuotaConfirmationForm):
|
||||
)
|
||||
cancellation_fee = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
localize=True,
|
||||
label=_('Keep a cancellation fee of'),
|
||||
help_text=_('If you keep a fee, all positions within this order will be canceled and the order will be reduced '
|
||||
@@ -213,7 +213,7 @@ class MarkPaidForm(ConfirmPaymentForm):
|
||||
)
|
||||
amount = forms.DecimalField(
|
||||
required=True,
|
||||
max_digits=10, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
localize=True,
|
||||
label=_('Payment amount'),
|
||||
)
|
||||
@@ -318,7 +318,7 @@ class OrderPositionAddForm(forms.Form):
|
||||
)
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
localize=True,
|
||||
label=_('Gross price'),
|
||||
help_text=_("Including taxes, if any. Keep empty for the product's default price")
|
||||
@@ -444,7 +444,7 @@ class OrderPositionChangeForm(forms.Form):
|
||||
)
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
localize=True,
|
||||
label=_('New price (gross)')
|
||||
)
|
||||
@@ -566,7 +566,7 @@ class OrderPositionChangeForm(forms.Form):
|
||||
class OrderFeeChangeForm(forms.Form):
|
||||
value = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
localize=True,
|
||||
label=_('New price (gross)')
|
||||
)
|
||||
@@ -731,7 +731,7 @@ class OrderRefundForm(forms.Form):
|
||||
)
|
||||
)
|
||||
partial_amount = forms.DecimalField(
|
||||
required=False, max_digits=10, decimal_places=2,
|
||||
required=False, max_digits=13, decimal_places=2,
|
||||
localize=True
|
||||
)
|
||||
|
||||
@@ -820,18 +820,18 @@ class EventCancelForm(forms.Form):
|
||||
)
|
||||
keep_fee_fixed = forms.DecimalField(
|
||||
label=_("Keep a fixed cancellation fee"),
|
||||
max_digits=10, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
required=False
|
||||
)
|
||||
keep_fee_per_ticket = forms.DecimalField(
|
||||
label=_("Keep a fixed cancellation fee per ticket"),
|
||||
help_text=_("Free tickets and add-on products are not counted"),
|
||||
max_digits=10, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
required=False
|
||||
)
|
||||
keep_fee_percentage = forms.DecimalField(
|
||||
label=_("Keep a percentual cancellation fee"),
|
||||
max_digits=10, decimal_places=2,
|
||||
max_digits=13, decimal_places=2,
|
||||
required=False
|
||||
)
|
||||
keep_fees = forms.MultipleChoiceField(
|
||||
|
||||
@@ -578,7 +578,8 @@ class WebHookForm(forms.ModelForm):
|
||||
class GiftCardCreateForm(forms.ModelForm):
|
||||
value = forms.DecimalField(
|
||||
label=_('Gift card value'),
|
||||
min_value=Decimal('0.00')
|
||||
min_value=Decimal('0.00'),
|
||||
max_value=Decimal('99999999.99'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="table-responsive">
|
||||
<span class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -146,6 +146,10 @@
|
||||
<span class="fa fa-qrcode fa-fw"></span>
|
||||
<span title="{{ c.raw_barcode }}">
|
||||
{{ c.raw_barcode|slice:":16" }}{% if c.raw_barcode|length > 16 %}…{% endif %}
|
||||
<button type="button" class="btn btn-xs btn-link btn-clipboard" data-clipboard-text="{{ c.raw_barcode }}">
|
||||
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
||||
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
|
||||
</button>
|
||||
</span>
|
||||
{% if c.raw_item %}
|
||||
<br>
|
||||
|
||||
@@ -67,6 +67,8 @@
|
||||
{% bootstrap_field form.change_allow_user_addons layout="control" %}
|
||||
{% bootstrap_field form.change_allow_user_until layout="control" %}
|
||||
{% bootstrap_field form.change_allow_user_price layout="control" %}
|
||||
{% bootstrap_field form.change_allow_user_if_checked_in layout="control" %}
|
||||
{% bootstrap_field form.change_allow_attendee layout="control" %}
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -315,7 +315,10 @@
|
||||
{% if sform.event_list_available_only %}
|
||||
{% bootstrap_field sform.event_list_available_only layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field sform.low_availability_percentage layout="control" %}
|
||||
{% if sform.event_calendar_future_only %}
|
||||
{% bootstrap_field sform.event_calendar_future_only layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
|
||||
|
||||
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
|
||||
{% propagated request.event org_url "meta_noindex" %}
|
||||
|
||||
@@ -515,6 +515,16 @@
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Check-in lists" %}</legend>
|
||||
<p class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
You can choose to either add one or more check-in lists for every date in your series individually,
|
||||
or use just one check-in list for all your dates and limit admission through check-in rules. Which
|
||||
approach is better depends on multiple factors, such as the number of dates in your series. For a
|
||||
series with one or less event date per day, individual lists are usually more helpful. If you
|
||||
use dates to represent many time slots on the same day, or even overlapping time slots, working with
|
||||
just one large check-in list will be easier.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
|
||||
{{ cl_formset.management_form }}
|
||||
{% bootstrap_formset_errors cl_formset %}
|
||||
|
||||
@@ -157,6 +157,16 @@
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Check-in lists" %}</legend>
|
||||
<p class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
You can choose to either add one or more check-in lists for every date in your series individually,
|
||||
or use just one check-in list for all your dates and limit admission through check-in rules. Which
|
||||
approach is better depends on multiple factors, such as the number of dates in your series. For a
|
||||
series with one or less event date per day, individual lists are usually more helpful. If you
|
||||
use dates to represent many time slots on the same day, or even overlapping time slots, working with
|
||||
just one large check-in list will be easier.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
|
||||
{{ cl_formset.management_form }}
|
||||
{% bootstrap_formset_errors cl_formset %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block title %}{% trans "Voucher" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Create multiple vouchers" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal" data-asynctask>
|
||||
<form action="" method="post" class="form-horizontal" data-asynctask mail-preview-url="{% url "control:event.vouchers.bulk.mail_preview" event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<fieldset>
|
||||
@@ -82,8 +82,54 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Send out emails" %}</legend>
|
||||
{% bootstrap_field form.send layout="control" %}
|
||||
{% bootstrap_field form.send_subject layout="horizontal" %}
|
||||
{% bootstrap_field form.send_message layout="horizontal" %}
|
||||
|
||||
<div id="send_subject_panel" class="preview-panel form-group" for="send_subject">
|
||||
{% with field=form.send_subject item="send_subject" %}
|
||||
<label class="col-md-3 control-label">{{ field.label }}</label>
|
||||
<div class="col-md-9">
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation" class="active">
|
||||
<a data-toggle="tab" type="edit" href="#{{ item }}_edit"><i class="fa fa-pencil-square-o fa-fw"></i> {% trans "Edit" %}</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a data-toggle="tab" type="preview" href="#{{ item }}_preview"><i class="fa fa-tv fa-fw"></i> {% trans "Preview" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div id="{{ item }}_edit" class="tab-pane fade in active">
|
||||
{% bootstrap_field field show_label=False form_group_class="" %}
|
||||
</div>
|
||||
<div id="{{ item }}_preview" class="tab-pane mail-preview-group">
|
||||
<div lang="all" for="{{ item }}" class="mail-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div id="send_message_panel" class="preview-panel form-group" for="send_message">
|
||||
{% with field=form.send_message item="send_message" %}
|
||||
<label class="col-md-3 control-label">{{ field.label }}</label>
|
||||
<div class="col-md-9">
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation" class="active">
|
||||
<a data-toggle="tab" type="edit" href="#{{ item }}_edit"><i class="fa fa-pencil-square-o fa-fw"></i> {% trans "Edit" %}</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a data-toggle="tab" type="preview" href="#{{ item }}_preview"><i class="fa fa-tv fa-fw"></i> {% trans "Preview" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div id="{{ item }}_edit" class="tab-pane fade in active">
|
||||
{% bootstrap_field field show_label=False form_group_class="" %}
|
||||
</div>
|
||||
<div id="{{ item }}_preview" class="tab-pane mail-preview-group">
|
||||
<div lang="all" for="{{ item }}" class="mail-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% bootstrap_field form.send_recipients layout="horizontal" %}
|
||||
</fieldset>
|
||||
{% eventsignal request.event "pretix.control.signals.voucher_form_html" form=form %}
|
||||
|
||||
@@ -177,6 +177,7 @@ urlpatterns = [
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/webhook/(?P<webhook>[^/]+)/logs$', organizer.WebHookLogsView.as_view(),
|
||||
name='organizer.webhook.logs'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/devices$', organizer.DeviceListView.as_view(), name='organizer.devices'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/devices/select2$', typeahead.devices_select2, name='organizer.devices.select2'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/device/add$', organizer.DeviceCreateView.as_view(),
|
||||
name='organizer.device.add'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/device/bulk_edit$', organizer.DeviceBulkUpdateView.as_view(),
|
||||
@@ -190,6 +191,7 @@ urlpatterns = [
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/logs$', organizer.DeviceLogView.as_view(),
|
||||
name='organizer.device.logs'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/gates$', organizer.GateListView.as_view(), name='organizer.gates'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/gates/select2$', typeahead.gate_select2, name='organizer.gates.select2'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/gate/add$', organizer.GateCreateView.as_view(), name='organizer.gate.add'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/gate/(?P<gate>[^/]+)/edit$', organizer.GateUpdateView.as_view(),
|
||||
name='organizer.gate.edit'),
|
||||
@@ -271,6 +273,7 @@ urlpatterns = [
|
||||
re_path(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'),
|
||||
re_path(r'^items/typeahead/meta/$', typeahead.item_meta_values, name='event.items.meta.typeahead'),
|
||||
re_path(r'^items/select2$', typeahead.items_select2, name='event.items.select2'),
|
||||
re_path(r'^items/select2/itemvar$', typeahead.itemvar_select2, name='event.items.itemvar.select2'),
|
||||
re_path(r'^items/select2/variation$', typeahead.variations_select2, name='event.items.variations.select2'),
|
||||
re_path(r'^categories/$', item.CategoryList.as_view(), name='event.items.categories'),
|
||||
re_path(r'^categories/select2$', typeahead.category_select2, name='event.items.categories.select2'),
|
||||
@@ -321,6 +324,7 @@ urlpatterns = [
|
||||
re_path(r'^vouchers/add$', vouchers.VoucherCreate.as_view(), name='event.vouchers.add'),
|
||||
re_path(r'^vouchers/go$', vouchers.VoucherGo.as_view(), name='event.vouchers.go'),
|
||||
re_path(r'^vouchers/bulk_add$', vouchers.VoucherBulkCreate.as_view(), name='event.vouchers.bulk'),
|
||||
re_path(r'^vouchers/bulk_add/mail_preview$', vouchers.VoucherBulkMailPreview.as_view(), name='event.vouchers.bulk.mail_preview'),
|
||||
re_path(r'^vouchers/bulk_action$', vouchers.VoucherBulkAction.as_view(), name='event.vouchers.bulkaction'),
|
||||
re_path(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),
|
||||
name='event.order.transition'),
|
||||
|
||||
@@ -712,8 +712,11 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
ctx = {}
|
||||
for p in get_available_placeholders(self.request.event, MailSettingsForm.base_context[item]).values():
|
||||
s = str(p.render_sample(self.request.event))
|
||||
if s.strip().startswith('*'):
|
||||
ctx[p.identifier] = s
|
||||
if s.strip().startswith('* '):
|
||||
ctx[p.identifier] = '<div class="placeholder" title="{}">{}</div>'.format(
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
markdown_compile_email(s)
|
||||
)
|
||||
else:
|
||||
ctx[p.identifier] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
|
||||
@@ -306,10 +306,11 @@ class EventWizard(SafeSessionWizardView):
|
||||
event.set_active_plugins(settings.PRETIX_PLUGINS_DEFAULT.split(","),
|
||||
allow_restricted=settings.PRETIX_PLUGINS_DEFAULT.split(","))
|
||||
event.save(update_fields=['plugins'])
|
||||
event.checkin_lists.create(
|
||||
name=_('Default'),
|
||||
all_products=True
|
||||
)
|
||||
if not event.has_subevents:
|
||||
event.checkin_lists.create(
|
||||
name=_('Default'),
|
||||
all_products=True
|
||||
)
|
||||
event.set_defaults()
|
||||
|
||||
if basics_data['tax_rate']:
|
||||
|
||||
@@ -2276,7 +2276,7 @@ class ExportMixin:
|
||||
continue
|
||||
|
||||
if self.scheduled:
|
||||
initial = self.scheduled.export_form_data
|
||||
initial = dict(self.scheduled.export_form_data)
|
||||
|
||||
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
|
||||
test_form.fields = ex.export_form_fields
|
||||
|
||||
@@ -1550,7 +1550,7 @@ class ExportMixin:
|
||||
if id != ex.identifier:
|
||||
continue
|
||||
if self.scheduled:
|
||||
initial = self.scheduled.export_form_data
|
||||
initial = dict(self.scheduled.export_form_data)
|
||||
|
||||
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
|
||||
test_form.fields = ex.export_form_fields
|
||||
|
||||
@@ -251,7 +251,9 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
'include_pending': False,
|
||||
}
|
||||
]
|
||||
extra = 0
|
||||
|
||||
if not self.request.event.checkin_lists.filter(subevent__isnull=True).exists():
|
||||
extra = 1
|
||||
|
||||
formsetclass = inlineformset_factory(
|
||||
SubEvent, CheckinList,
|
||||
|
||||
@@ -545,6 +545,47 @@ def checkinlist_select2(request, **kwargs):
|
||||
return JsonResponse(doc)
|
||||
|
||||
|
||||
@event_permission_required(None)
|
||||
def itemvar_select2(request, **kwargs):
|
||||
query = request.GET.get('query', '')
|
||||
try:
|
||||
page = int(request.GET.get('page', '1'))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
pagesize = 20
|
||||
offset = (page - 1) * pagesize
|
||||
|
||||
choices = []
|
||||
|
||||
# We are very unlikely to need pagination
|
||||
itemqs = request.event.items.prefetch_related('variations').filter(Q(name__icontains=i18ncomp(query)) | Q(internal_name__icontains=query))
|
||||
total = itemqs.count()
|
||||
|
||||
for i in itemqs[offset:offset + pagesize]:
|
||||
variations = list(i.variations.all())
|
||||
if variations:
|
||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=i), not i.active))
|
||||
for v in variations:
|
||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i, v.value), not v.active))
|
||||
else:
|
||||
choices.append((str(i.pk), str(i), not i.active))
|
||||
|
||||
doc = {
|
||||
'results': [
|
||||
{
|
||||
'id': k,
|
||||
'text': str(v),
|
||||
}
|
||||
for k, v, d in choices
|
||||
],
|
||||
'pagination': {
|
||||
"more": total >= (offset + pagesize)
|
||||
}
|
||||
}
|
||||
return JsonResponse(doc)
|
||||
|
||||
|
||||
@event_permission_required(None)
|
||||
def itemvarquota_select2(request, **kwargs):
|
||||
query = request.GET.get('query', '')
|
||||
@@ -831,3 +872,71 @@ def item_meta_values(request, organizer, event):
|
||||
)
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@organizer_permission_required(("can_view_orders", "can_change_organizer_settings"))
|
||||
# This decorator is a bit of a hack since this is not technically an organizer permission, but it does the job here --
|
||||
# anyone who can see orders for any event can see the check-in log view where this is used as a filter
|
||||
def devices_select2(request, **kwargs):
|
||||
query = request.GET.get('query', '')
|
||||
try:
|
||||
page = int(request.GET.get('page', '1'))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
qq = (
|
||||
Q(name__icontains=query) | Q(hardware_brand__icontains=query) | Q(hardware_model__icontains=query) |
|
||||
Q(unique_serial__istartswith=query)
|
||||
)
|
||||
try:
|
||||
qq |= Q(device_id=int(query))
|
||||
except ValueError:
|
||||
pass
|
||||
qs = request.organizer.devices.filter(qq).order_by('device_id')
|
||||
|
||||
total = qs.count()
|
||||
pagesize = 20
|
||||
offset = (page - 1) * pagesize
|
||||
doc = {
|
||||
'results': [
|
||||
{
|
||||
'id': e.pk,
|
||||
'text': str(e),
|
||||
}
|
||||
for e in qs[offset:offset + pagesize]
|
||||
],
|
||||
'pagination': {
|
||||
"more": total >= (offset + pagesize)
|
||||
}
|
||||
}
|
||||
return JsonResponse(doc)
|
||||
|
||||
|
||||
@organizer_permission_required(("can_view_orders", "can_change_organizer_settings"))
|
||||
# This decorator is a bit of a hack since this is not technically an organizer permission, but it does the job here --
|
||||
# anyone who can see orders for any event can see the check-in log view where this is used as a filter
|
||||
def gate_select2(request, **kwargs):
|
||||
query = request.GET.get('query', '')
|
||||
try:
|
||||
page = int(request.GET.get('page', '1'))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
qs = request.organizer.gates.filter(Q(name__icontains=query) | Q(identifier__icontains=query)).order_by('name')
|
||||
|
||||
total = qs.count()
|
||||
pagesize = 20
|
||||
offset = (page - 1) * pagesize
|
||||
doc = {
|
||||
'results': [
|
||||
{
|
||||
'id': e.pk,
|
||||
'text': str(e),
|
||||
}
|
||||
for e in qs[offset:offset + pagesize]
|
||||
],
|
||||
'pagination': {
|
||||
"more": total >= (offset + pagesize)
|
||||
}
|
||||
}
|
||||
return JsonResponse(doc)
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
import io
|
||||
|
||||
import bleach
|
||||
from defusedcsv import csv
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
@@ -56,10 +57,12 @@ from django.views.generic import (
|
||||
CreateView, ListView, TemplateView, UpdateView, View,
|
||||
)
|
||||
|
||||
from pretix.base.email import get_available_placeholders
|
||||
from pretix.base.models import CartPosition, LogEntry, Voucher
|
||||
from pretix.base.models.vouchers import generate_codes
|
||||
from pretix.base.services.locking import NoLockManager
|
||||
from pretix.base.services.vouchers import vouchers_send
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.base.views.tasks import AsyncFormView
|
||||
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
|
||||
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
|
||||
@@ -67,6 +70,7 @@ from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.signals import voucher_form_class
|
||||
from pretix.control.views import PaginationMixin
|
||||
from pretix.helpers.compat import CompatDeleteView
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
|
||||
@@ -509,6 +513,52 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class VoucherBulkMailPreview(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_vouchers'
|
||||
|
||||
# return the origin text if key is missing in dict
|
||||
class SafeDict(dict):
|
||||
def __missing__(self, key):
|
||||
return '{' + key + '}'
|
||||
|
||||
# get all supported placeholders with dummy values
|
||||
def placeholders(self, item):
|
||||
ctx = {}
|
||||
base_ctx = ['event', 'name']
|
||||
if item == 'send_message':
|
||||
base_ctx += ['voucher_list']
|
||||
for p in get_available_placeholders(self.request.event, base_ctx).values():
|
||||
s = str(p.render_sample(self.request.event))
|
||||
if s.strip().startswith('* ') or s.startswith(' '):
|
||||
ctx[p.identifier] = '<div class="placeholder" title="{}">{}</div>'.format(
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
markdown_compile_email(s)
|
||||
)
|
||||
else:
|
||||
ctx[p.identifier] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
s
|
||||
)
|
||||
return self.SafeDict(ctx)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
preview_item = request.POST.get('item', '')
|
||||
if preview_item not in ('send_message', 'send_subject'):
|
||||
return HttpResponseBadRequest(_('invalid item'))
|
||||
msgs = {}
|
||||
if "subject" in preview_item:
|
||||
msgs["all"] = format_map(bleach.clean(request.POST.get(preview_item, "")), self.placeholders(preview_item))
|
||||
else:
|
||||
msgs["all"] = markdown_compile_email(
|
||||
format_map(request.POST.get(preview_item), self.placeholders(preview_item))
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'item': preview_item,
|
||||
'msgs': msgs
|
||||
})
|
||||
|
||||
|
||||
class VoucherRNG(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_vouchers'
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import re
|
||||
import types
|
||||
from inspect import isgenerator
|
||||
|
||||
from openpyxl import Workbook
|
||||
@@ -140,9 +141,9 @@ class SafeWorkbook(Workbook):
|
||||
# monkeypatch existing sheets
|
||||
for s in self._sheets:
|
||||
if self.write_only:
|
||||
s.append = SafeWriteOnlyWorksheet.append
|
||||
s.append = types.MethodType(SafeWriteOnlyWorksheet.append, s)
|
||||
else:
|
||||
s.append = SafeWorksheet.append
|
||||
s.append = types.MethodType(SafeWorksheet.append, s)
|
||||
|
||||
def create_sheet(self, title=None, index=None):
|
||||
if self.read_only:
|
||||
|
||||
+1147
-1080
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
+1128
-1071
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
+1394
-1355
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2023-01-13 08:00+0000\n"
|
||||
"Last-Translator: tree <jan.stransky@arnal.cz>\n"
|
||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
+1126
-1071
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2022-12-01 17:00+0000\n"
|
||||
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
+1162
-1084
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2023-02-09 13:56+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
@@ -333,6 +333,7 @@ Veranstaltername
|
||||
Veranstalterprofil
|
||||
Veranstalterseite
|
||||
Veranstalterübersicht
|
||||
veranstalterweiten
|
||||
Veranstaltungs
|
||||
veranstaltungsweiten
|
||||
Verfügbarkeitsberechnung
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2023-02-09 13:56+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
|
||||
@@ -333,6 +333,7 @@ Veranstaltername
|
||||
Veranstalterprofil
|
||||
Veranstalterseite
|
||||
Veranstalterübersicht
|
||||
veranstalterweiten
|
||||
Veranstaltungs
|
||||
veranstaltungsweiten
|
||||
Verfügbarkeitsberechnung
|
||||
|
||||
+1109
-1058
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
+1158
-1100
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
|
||||
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
|
||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
+1144
-1089
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2021-11-25 21:00+0000\n"
|
||||
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
+1155
-1084
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
|
||||
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
+1124
-1069
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: French\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2023-01-17 01:00+0000\n"
|
||||
"Last-Translator: Maurice Kaag <maurice@kaag.me>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
+1135
-1078
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2022-02-22 22:00+0000\n"
|
||||
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
|
||||
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
+1109
-1058
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2021-09-24 13:54+0000\n"
|
||||
"Last-Translator: ofirtro <ofir.tro@gmail.com>\n"
|
||||
"Language-Team: Hebrew <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
+1109
-1058
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
|
||||
+1119
-1064
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2020-01-24 08:00+0000\n"
|
||||
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
|
||||
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
+1148
-1080
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2022-05-08 19:00+0000\n"
|
||||
"Last-Translator: Emanuele Signoretta <signorettae@gmail.com>\n"
|
||||
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
+1109
-1058
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2022-03-15 00:00+0000\n"
|
||||
"Last-Translator: Yuriko Matsunami <y.matsunami@enobyte.com>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
+1150
-1079
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2022-04-06 03:00+0000\n"
|
||||
"Last-Translator: Liga V <lerning_by_dreaming@gmx.de>\n"
|
||||
"Language-Team: Latvian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2022-06-20 02:00+0000\n"
|
||||
"Last-Translator: fyksen <fredrik@fyksen.me>\n"
|
||||
"Language-Team: Norwegian Bokmål <https://translate.pretix.eu/projects/pretix/"
|
||||
|
||||
+1151
-1087
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2021-10-29 02:00+0000\n"
|
||||
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-02-27 08:00+0000\n"
|
||||
"POT-Creation-Date: 2023-03-08 15:04+0000\n"
|
||||
"PO-Revision-Date: 2021-08-05 04:00+0000\n"
|
||||
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
|
||||
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user