Compare commits

...

69 Commits

Author SHA1 Message Date
Richard Schreiber 3d220cce0f Widget: fix close-icon position 2023-03-21 08:43:45 +01:00
Raphael Michel 17574e8a23 Add email placeholder positionid 2023-03-20 17:26:49 +01:00
Raphael Michel de7314edcc Tax rule editor: Fix label for "text on invoice" 2023-03-20 17:19:32 +01:00
Raphael Michel df25a1cebf Invoice renderer: Line break on very large amounts 2023-03-20 16:46:44 +01:00
Raphael Michel 0578955273 API: Fix crash on invalid input 2023-03-20 16:46:44 +01:00
Raphael Michel 0b4daa9b16 Do not use "nonce" in user-exposed strings, as it is a slur in Britisch Englisch 2023-03-17 22:07:37 +01:00
Raphael Michel 8dfc77a927 Manual payment: Support for "invoice immediately" 2023-03-17 15:25:51 +01:00
Raphael Michel d0f603283b OrderChangeManager: Fix invoice regeneration on tax rate change 2023-03-17 13:41:18 +01:00
Raphael Michel 3df61b8fb5 Remove debug output 2023-03-17 13:32:52 +01:00
Raphael Michel fdead71884 Optionally allow self-service order changes after check-in 2023-03-17 09:22:44 +01:00
Raphael Michel 369251b0b0 Fix confusion between tax rate and tax value 2023-03-16 22:47:23 +01:00
Raphael Michel e83798a9b7 Increase validated size of prices 2023-03-16 21:33:06 +01:00
Raphael Michel 4c9640561c Increase size of monetary decimal fields 2023-03-16 21:26:37 +01:00
Raphael Michel e9ab0d8654 OrderChangeManager: Fix typo 2023-03-15 16:38:19 +01:00
Raphael Michel c9e5cce7d0 Voucher bulk creation: Add markdown preview for email field (#3143) 2023-03-15 15:55:34 +01:00
Raphael Michel 859004ec59 Use more sensible defaults for check-in lists in event series (#3147) 2023-03-14 22:26:40 +01:00
Raphael Michel 136511f394 Order.send_email: Support for attach_cached_files 2023-03-14 21:46:31 +01:00
Raphael Michel ee4081d9c3 API: Allow to filter events by testmode attribute 2023-03-10 16:34:54 +01:00
Raphael Michel 28b4982161 Check-in log: Add select2 for item selection 2023-03-10 15:46:27 +01:00
Raphael Michel 5c8a3f18f3 Check-in log: Add select2 for gate selection 2023-03-10 15:21:25 +01:00
Raphael Michel 440d1b5766 Export: Fix inconsistent file name 2023-03-10 15:17:31 +01:00
Raphael Michel 1ff8c6f78b PDF renderer: Place hidden text inside page rect 2023-03-10 14:26:50 +01:00
Raphael Michel 71e6a85c38 Make it easier to copy ticket secrets to clipboard 2023-03-09 18:15:54 +01:00
Raphael Michel 497e6f5c8f Check-in log: Add select2 for device selection 2023-03-09 18:15:39 +01:00
Raphael Michel 1d60827fa1 Check-in log export: Allow to filter by date range 2023-03-09 17:48:43 +01:00
Raphael Michel 3bbed98844 Fix a potentially destructive bug in 61ae434ab 2023-03-08 23:48:45 +01:00
Raphael Michel 8b8ad34d30 Order change: Should not set user for order change manager in presale 2023-03-08 23:32:12 +01:00
Raphael Michel e64034ed90 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5186 of 5186 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2023-03-08 16:37:47 +01:00
Raphael Michel b185dce17c Translations: Update German
Currently translated at 100.0% (5186 of 5186 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2023-03-08 16:37:47 +01:00
Raphael Michel 2ffa2315ca Translations: Add word to spellcheck ist 2023-03-08 16:28:07 +01:00
Raphael Michel b0616ed00d Scheduled exports: Fix serialization problem 2023-03-08 16:09:33 +01:00
Raphael Michel c36b9bcfcd Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-03-08 16:04:03 +01:00
Raphael Michel 2533ae2b3a order_overview: Add base_fees_qs parameter 2023-03-08 16:03:29 +01:00
Raphael Michel 61ae434ab1 Allow attendees to change selected add-ons of same price (#3150) 2023-03-08 16:01:59 +01:00
alemao8 2ebbe82baf Translations: Update Greek
Currently translated at 54.4% (2820 of 5177 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/el/

powered by weblate
2023-03-08 15:19:20 +01:00
Michael 153eb67300 Translations: Update Czech
Currently translated at 28.9% (1499 of 5177 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/cs/

powered by weblate
2023-03-08 15:19:20 +01:00
Raphael Michel a2ed32be8b Fix buggy check in question answer shredder 2023-03-07 11:40:36 +01:00
Raphael Michel b5c94fd002 Fix changing order in extended order search 2023-03-07 11:03:36 +01:00
Raphael Michel 1b02a898a1 Fix TypeError in OAuth authorization 2023-03-07 10:43:07 +01:00
Raphael Michel f29aa73f8d Event series calendar: Allow to hide all past events (#3142) 2023-03-06 18:25:45 +01:00
Raphael Michel 62cbed4891 Merge branch 'security-20230306' 2023-03-06 14:36:33 +01:00
Raphael Michel 68e31b92fe Make shredder code more robust 2023-03-06 14:25:59 +01:00
Raphael Michel 9a90444cca OAuth: Log authorized organizers 2023-03-06 14:25:59 +01:00
Michael 18f57ea012 Translations: Update Czech
Currently translated at 25.8% (1337 of 5177 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/cs/

powered by weblate
2023-03-06 12:06:52 +01:00
Richard Schreiber 08a85b3dab Order edit form: fix yes/no-question not being optional (Z#23117305) (#3149) 2023-03-06 12:03:08 +01:00
Raphael Michel 81a5e263cb Allow to set privacy policy URL per language (#3146) 2023-03-06 12:02:52 +01:00
Raphael Michel 926d334b10 [SECURITY] Enforce session validation on oauth authorize endpoint 2023-03-06 11:52:01 +01:00
Raphael Michel 9bed40fa09 Add name of waiting list entry to voucher 2023-03-03 14:42:32 +01:00
Raphael Michel ed1dae5fde API: Fix duplicate results when filtering orders 2023-03-03 14:36:40 +01:00
Richard Schreiber c7060d188f Product list: Indent variations, improve alignment of price and quantity (#3135) 2023-03-03 14:26:11 +01:00
Raphael Michel 9a53dc9c5e Move from PyPDF2 to pypdf again 2023-03-03 14:06:01 +01:00
dependabot[bot] d1e0a7293b Bump @babel/core from 7.20.12 to 7.21.0 in /src/pretix/static/npm_dir (#3140)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-02 14:06:59 +01:00
Raphael Michel 7d5b1eebcb Fix type error in SafeWorksheet 2023-03-02 11:54:49 +01:00
Raphael Michel e4a02c494e Event settings: Clarify low_availability_percentage is a percentage 2023-03-02 10:56:24 +01:00
Raphael Michel 6ecac70727 shred_log_fields: Only mark shredded if something happened 2023-03-02 10:56:14 +01:00
Raphael Michel 8a1554323e Mail settings: Fix preview for mail_text_order_pending_warning 2023-03-01 16:31:52 +01:00
Raphael Michel cfc22c806a Fix possible crash in OrderChangeManager (PRETIXEU-7ZY) 2023-03-01 13:55:07 +01:00
Raphael Michel f70d6877dc Limit maximum value of transaction at gift card creation (PRETIXEU-870) 2023-03-01 13:32:03 +01:00
Raphael Michel 4dfad2ef42 Translations: Update Portuguese (Portugal)
Currently translated at 93.8% (4860 of 5177 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_PT/

powered by weblate
2023-03-01 10:51:56 +01:00
Raphael Michel dc1d35cb1f Translations: Update Portuguese (Portugal)
Currently translated at 93.8% (4861 of 5177 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_PT/

powered by weblate
2023-03-01 10:51:56 +01:00
Raphael Michel 6e657db882 Translations: Update Portuguese (Portugal)
Currently translated at 93.8% (4861 of 5177 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_PT/

powered by weblate
2023-03-01 10:51:56 +01:00
Raphael Michel 91d3aaf20b Translations: Update Italian
Currently translated at 18.1% (938 of 5177 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/it/

powered by weblate
2023-03-01 10:51:56 +01:00
Raphael Michel 2f1318e2b9 Translations: Update Portuguese (Portugal)
Currently translated at 93.8% (4861 of 5177 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_PT/

powered by weblate
2023-03-01 10:51:56 +01:00
Daniel Lopez 844a74d33f Translations: Update Finnish
Currently translated at 17.0% (885 of 5177 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2023-03-01 10:51:56 +01:00
Vasco Baleia 76788a874a Translations: Update Portuguese (Portugal)
Currently translated at 93.9% (4862 of 5177 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pt_PT/

powered by weblate
2023-03-01 10:51:56 +01:00
Michele Pagnozzi e29d5c37cd Translations: Update Italian
Currently translated at 18.1% (938 of 5177 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/it/

powered by weblate
2023-03-01 10:51:56 +01:00
Raphael Michel 258e66587e Address form: Fix state selection for Mexico to include Mexico City 2023-03-01 09:57:12 +01:00
Raphael Michel d13af2eeab Translations: Use more neutral term for taxes in German due to different terminology in Switzerland 2023-02-27 12:13:06 +01:00
Raphael Michel 14b9afcc40 Bump version to 4.18.0.dev0 2023-02-27 09:27:42 +01:00
175 changed files with 43632 additions and 40891 deletions
+1
View File
@@ -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.
+1 -1
View File
@@ -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"
+2
View File
@@ -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',
+15
View File
@@ -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
+5 -5
View File
@@ -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
+1 -3
View File
@@ -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='*')
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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 = (
+3 -1
View File
@@ -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)
+4 -4
View File
@@ -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:
+1 -1
View File
@@ -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(
+4
View File
@@ -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,
+3 -1
View File
@@ -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
+16
View File
@@ -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
+1 -1
View File
@@ -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'})
+8 -5
View File
@@ -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),
),
]
+6 -1
View File
@@ -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 '
+1 -1
View File
@@ -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'),
)
+1 -1
View File
@@ -129,7 +129,7 @@ class GiftCardTransaction(models.Model):
)
value = models.DecimalField(
decimal_places=2,
max_digits=10
max_digits=13
)
order = models.ForeignKey(
'Order',
+3 -3
View File
@@ -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)
+7 -7
View File
@@ -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 '
+45 -21
View File
@@ -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)
+3 -3
View File
@@ -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(
+4 -1
View File
@@ -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,
+10
View File
@@ -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
+3 -3
View File
@@ -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):
+4 -4
View File
@@ -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
))
+31 -4
View File
@@ -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(
+1 -1
View File
@@ -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)
+3 -2
View File
@@ -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
View File
@@ -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'),
}
+8 -4
View File
@@ -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'])
+10 -2
View File
@@ -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={
+38 -3
View File
@@ -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
+9 -9
View File
@@ -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(
+2 -1
View File
@@ -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):
+1 -1
View File
@@ -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 %}
+4
View File
@@ -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'),
+5 -2
View File
@@ -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.'),
+5 -4
View File
@@ -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']:
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+3 -1
View File
@@ -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,
+109
View File
@@ -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)
+50
View File
@@ -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'
+3 -2
View File
@@ -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:
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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/"
+1
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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