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.0"
|
||||
__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 = (
|
||||
|
||||
@@ -34,6 +34,7 @@ from oauth2_provider.views import (
|
||||
|
||||
from pretix.api.models import OAuthApplication
|
||||
from pretix.base.models import Organizer
|
||||
from pretix.control.views.user import RecentAuthenticationRequiredMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,7 +55,7 @@ class OAuthAllowForm(AllowForm):
|
||||
del self.fields['organizers']
|
||||
|
||||
|
||||
class AuthorizationView(BaseAuthorizationView):
|
||||
class AuthorizationView(RecentAuthenticationRequiredMixin, BaseAuthorizationView):
|
||||
template_name = "pretixcontrol/auth/oauth_authorization.html"
|
||||
form_class = OAuthAllowForm
|
||||
|
||||
@@ -111,6 +112,7 @@ class AuthorizationView(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):
|
||||
|
||||
@@ -112,7 +112,7 @@ class PermissionMiddleware:
|
||||
url = resolve(request.path_info)
|
||||
url_name = url.url_name
|
||||
|
||||
if not request.path.startswith(get_script_prefix() + 'control'):
|
||||
if not request.path.startswith(get_script_prefix() + 'control') and not (url.namespace.startswith("api-") and url_name == "authorize"):
|
||||
# This middleware should only touch the /control subpath
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user