mirror of
https://github.com/pretix/pretix.git
synced 2025-12-05 21:32:28 +00:00
Compare commits
17 Commits
voucher-we
...
hierarkey-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8baacda154 | ||
|
|
bbc54a3852 | ||
|
|
e177bc7475 | ||
|
|
650b4b461f | ||
|
|
d14f7fb108 | ||
|
|
160f1c2e62 | ||
|
|
b9e627a86c | ||
|
|
328867c089 | ||
|
|
3e45274343 | ||
|
|
538ca9f0c2 | ||
|
|
99e10adad4 | ||
|
|
10b5f76356 | ||
|
|
39a0093c6b | ||
|
|
d8bf3d0b07 | ||
|
|
4e56ce8927 | ||
|
|
807df01f5d | ||
|
|
067e11c265 |
@@ -359,3 +359,65 @@ Performing a ticket search
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer or check-in list does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested check-in list does not exist.
|
||||
|
||||
.. _`rest-checkin-annul`:
|
||||
|
||||
Annulment of a check-in
|
||||
-----------------------
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/checkinrpc/annul/
|
||||
|
||||
If a check-in was made in error and the person was not let in, it can be annulled. We do not recommend this to be used
|
||||
in case of manual check-ins or user interfaces because it is too prone for human errors. It is mostly intended for
|
||||
automated entry systems like a turnstile or automated door, where the check-in is first created, then the door is
|
||||
opened, and then the check-in may be annulled if the system knows that the turnstile did not turn or was out of
|
||||
order.
|
||||
|
||||
This endpoint supports passing multiple check-in lists for the context of a multi-event scan. However, each
|
||||
check-in list passed needs to be from a distinct event.
|
||||
|
||||
Check-ins created by a device can only be annulled by the same device. The datetime of annulment may not be more than
|
||||
15 minutes after the datetime of check-in (value subject to change).
|
||||
|
||||
A status code of 404 is returned if no check-in was found for the given nonce. A status code of 400 is returned when
|
||||
multiple check-ins match the nonce, the input is invalid in another way, the annulment is made from the wrong device,
|
||||
the check-in is already in an annulled or failed state, or the datetime constraint is not valid.
|
||||
|
||||
:<json string nonce: ``nonce`` value of the original check-in.
|
||||
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
|
||||
:<json datetime datetime: Specifies the client-side datetime of the annulment. If not supplied, the current time will be used.
|
||||
:<json string error_explanation: A human-readable description of why the check-in was annulled (optional).
|
||||
:>json string status: ``"ok"``
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/checkinrpc/annul/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"lists": [1],
|
||||
"nonce": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
|
||||
"error_explanation": "Turnstile did not turn"
|
||||
}
|
||||
|
||||
**Example successful response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "ok",
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: Invalid or incomplete request, see above
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
:statuscode 404: The requested nonce does not exist.
|
||||
|
||||
@@ -1926,6 +1926,7 @@ Manipulating individual positions
|
||||
|
||||
(Full order position resource, see above.)
|
||||
|
||||
:query boolean check_quotas: Whether to check quotas before committing item changes, default is ``true``
|
||||
:param organizer: The ``slug`` field of the organizer of the event
|
||||
:param event: The ``slug`` field of the event
|
||||
:param id: The ``id`` field of the order position to update
|
||||
@@ -2005,6 +2006,7 @@ Manipulating individual positions
|
||||
|
||||
(Full order position resource, see above.)
|
||||
|
||||
:query boolean check_quotas: Whether to check quotas before creating the new position, default is ``true``
|
||||
:param organizer: The ``slug`` field of the organizer of the event
|
||||
:param event: The ``slug`` field of the event
|
||||
|
||||
@@ -2291,6 +2293,7 @@ otherwise, such as splitting an order or changing fees.
|
||||
|
||||
(Full order position resource, see above.)
|
||||
|
||||
:query boolean check_quotas: Whether to check quotas before patching or creating positions, default is ``true``
|
||||
:param organizer: The ``slug`` field of the organizer of the event
|
||||
:param event: The ``slug`` field of the event
|
||||
:param code: The ``code`` field of the order to update
|
||||
|
||||
@@ -14,6 +14,7 @@ The voucher resource contains the following public fields:
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the voucher
|
||||
created datetime The creation date of the voucher. For vouchers created before pretix 2025.7.0, this is guessed retroactively and might not be accurate.
|
||||
code string The voucher code that is required to redeem the voucher
|
||||
max_usages integer The maximum number of times this voucher can be
|
||||
redeemed (default: 1).
|
||||
@@ -84,6 +85,7 @@ Endpoints
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"created": "2020-09-18T14:17:40.971519Z",
|
||||
"code": "43K6LKM37FBVR2YG",
|
||||
"max_usages": 1,
|
||||
"redeemed": 0,
|
||||
@@ -156,6 +158,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"created": "2020-09-18T14:17:40.971519Z",
|
||||
"code": "43K6LKM37FBVR2YG",
|
||||
"max_usages": 1,
|
||||
"redeemed": 0,
|
||||
@@ -228,6 +231,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"created": "2020-09-18T14:17:40.971519Z",
|
||||
"code": "43K6LKM37FBVR2YG",
|
||||
"max_usages": 1,
|
||||
"redeemed": 0,
|
||||
@@ -321,6 +325,7 @@ Endpoints
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"created": "2020-09-18T14:17:40.971519Z",
|
||||
"code": "43K6LKM37FBVR2YG",
|
||||
…
|
||||
}, …
|
||||
@@ -367,6 +372,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"created": "2020-09-18T14:17:40.971519Z",
|
||||
"code": "43K6LKM37FBVR2YG",
|
||||
"max_usages": 1,
|
||||
"redeemed": 0,
|
||||
|
||||
@@ -30,7 +30,7 @@ Check-ins
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
:members: checkin_created
|
||||
:members: checkin_created, checkin_annulled
|
||||
|
||||
|
||||
Frontend
|
||||
|
||||
@@ -42,7 +42,7 @@ dependencies = [
|
||||
"django-filter==25.1",
|
||||
"django-formset-js-improved==0.5.0.3",
|
||||
"django-formtools==2.5.1",
|
||||
"django-hierarkey==1.2.*",
|
||||
"django-hierarkey==2.0.*",
|
||||
"django-hijack==3.7.*",
|
||||
"django-i18nfield==1.10.*",
|
||||
"django-libsass==0.9",
|
||||
@@ -81,14 +81,14 @@ dependencies = [
|
||||
"pycountry",
|
||||
"pycparser==2.22",
|
||||
"pycryptodome==3.23.*",
|
||||
"pypdf==5.9.*",
|
||||
"pypdf==6.0.*",
|
||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.9.*",
|
||||
"pytz",
|
||||
"pytz-deprecation-shim==0.1.*",
|
||||
"pyuca",
|
||||
"qrcode==8.2",
|
||||
"redis==6.3.*",
|
||||
"redis==6.4.*",
|
||||
"reportlab==4.4.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==2.34.*",
|
||||
@@ -110,7 +110,7 @@ dev = [
|
||||
"aiohttp==3.12.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.30.*",
|
||||
"fakeredis==2.31.*",
|
||||
"flake8==7.3.*",
|
||||
"freezegun",
|
||||
"isort==6.0.*",
|
||||
|
||||
@@ -104,3 +104,14 @@ class MiniCheckinListSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class CheckinRPCAnnulInputSerializer(serializers.Serializer):
|
||||
lists = serializers.PrimaryKeyRelatedField(required=True, many=True, queryset=CheckinList.objects.none())
|
||||
nonce = serializers.CharField(required=True, allow_null=False)
|
||||
datetime = serializers.DateTimeField(required=False, allow_null=True)
|
||||
error_explanation = serializers.CharField(required=False, allow_null=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['lists'].child_relation.queryset = CheckinList.objects.filter(event__in=self.context['events']).select_related('event')
|
||||
|
||||
@@ -83,6 +83,7 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
||||
|
||||
def create(self, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
check_quotas = self.context.get('check_quotas', True)
|
||||
|
||||
try:
|
||||
ocm.add_position(
|
||||
@@ -96,7 +97,7 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
||||
valid_until=validated_data.get('valid_until'),
|
||||
)
|
||||
if self.context.get('commit', True):
|
||||
ocm.commit()
|
||||
ocm.commit(check_quotas=check_quotas)
|
||||
return validated_data['order'].positions.order_by('-positionid').first()
|
||||
else:
|
||||
return OrderPosition() # fake to appease DRF
|
||||
@@ -310,6 +311,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
check_quotas = self.context.get('check_quotas', True)
|
||||
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
|
||||
item = validated_data.get('item', instance.item)
|
||||
variation = validated_data.get('variation', instance.variation)
|
||||
@@ -356,7 +358,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
||||
ocm.change_ticket_secret(instance, secret)
|
||||
|
||||
if self.context.get('commit', True):
|
||||
ocm.commit()
|
||||
ocm.commit(check_quotas=check_quotas)
|
||||
instance.refresh_from_db()
|
||||
except OrderError as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
@@ -70,7 +70,7 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Voucher
|
||||
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
||||
fields = ('id', 'created', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
|
||||
'all_bundles_included', 'budget', 'budget_used')
|
||||
|
||||
@@ -132,6 +132,8 @@ urlpatterns = [
|
||||
name="checkinrpc.redeem"),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/search/$', checkin.CheckinRPCSearchView.as_view(),
|
||||
name="checkinrpc.search"),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/annul/$', checkin.CheckinRPCAnnulView.as_view(),
|
||||
name="checkinrpc.annul"),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/settings/$', organizer.OrganizerSettingsView.as_view(),
|
||||
name="organizer.settings"),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/giftcards/(?P<giftcard>[^/]+)/', include(giftcard_router.urls)),
|
||||
|
||||
@@ -20,12 +20,13 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import operator
|
||||
from datetime import timedelta
|
||||
from functools import reduce
|
||||
|
||||
import django_filters
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError as BaseValidationError
|
||||
from django.db import transaction
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
|
||||
prefetch_related_objects,
|
||||
@@ -39,17 +40,19 @@ from django.utils.translation import gettext
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from packaging.version import parse
|
||||
from rest_framework import views, viewsets
|
||||
from rest_framework import status, views, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.exceptions import (
|
||||
NotFound, PermissionDenied, ValidationError,
|
||||
)
|
||||
from rest_framework.fields import DateTimeField
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework.permissions import SAFE_METHODS
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.serializers.checkin import (
|
||||
CheckinListSerializer, CheckinRPCRedeemInputSerializer,
|
||||
MiniCheckinListSerializer,
|
||||
CheckinListSerializer, CheckinRPCAnnulInputSerializer,
|
||||
CheckinRPCRedeemInputSerializer, MiniCheckinListSerializer,
|
||||
)
|
||||
from pretix.api.serializers.item import QuestionSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
@@ -66,6 +69,8 @@ from pretix.base.models.orders import PrintLog
|
||||
from pretix.base.services.checkin import (
|
||||
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||
)
|
||||
from pretix.base.signals import checkin_annulled
|
||||
from pretix.helpers import OF_SELF
|
||||
|
||||
with scopes_disabled():
|
||||
class CheckinListFilter(FilterSet):
|
||||
@@ -999,3 +1004,79 @@ class CheckinRPCSearchView(ListAPIView):
|
||||
qs = qs.none()
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class CheckinRPCAnnulView(views.APIView):
|
||||
def post(self, request, *args, **kwargs):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
|
||||
elif self.request.user.is_authenticated:
|
||||
events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
else:
|
||||
raise ValueError("unknown authentication method")
|
||||
|
||||
s = CheckinRPCAnnulInputSerializer(data=request.data, context={'events': events})
|
||||
s.is_valid(raise_exception=True)
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
qs = Checkin.all.all()
|
||||
if isinstance(request.auth, Device):
|
||||
qs = qs.filter(device=request.auth)
|
||||
ci = qs.select_for_update(
|
||||
of=OF_SELF,
|
||||
).select_related("position", "position__order", "position__order__event").get(
|
||||
list__in=s.validated_data['lists'],
|
||||
nonce=s.validated_data['nonce'],
|
||||
)
|
||||
if connection.features.has_select_for_update_of and ci.position_id:
|
||||
# Lock position as well, can't do it with of= above because relation is nullable
|
||||
OrderPosition.objects.select_for_update(of=OF_SELF).get(pk=ci.position_id)
|
||||
|
||||
if not ci.successful or not ci.position:
|
||||
raise ValidationError("Cannot annul an unsuccessful checkin")
|
||||
except Checkin.DoesNotExist:
|
||||
raise NotFound("No check-in found based on nonce")
|
||||
except Checkin.MultipleObjectsReturned:
|
||||
raise ValidationError("Multiple check-ins found based on nonce")
|
||||
|
||||
annulment_time = s.validated_data.get("datetime") or now()
|
||||
|
||||
if annulment_time - ci.datetime > timedelta(minutes=15):
|
||||
# Compare to sent datetime, which makes this cheatable, but allows offline annulment of checkins
|
||||
ci.position.order.log_action('pretix.event.checkin.annulment.ignored', data={
|
||||
'checkin': ci.pk,
|
||||
'position': ci.position.id,
|
||||
'positionid': ci.position.positionid,
|
||||
'datetime': annulment_time,
|
||||
'error_explanation': s.validated_data.get("error_explanation"),
|
||||
'type': ci.type,
|
||||
'list': ci.list_id,
|
||||
}, user=request.user, auth=request.auth)
|
||||
return Response({
|
||||
"non_field_errors": ["Annulment is not allowed more than 15 minutes after check-in"]
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if ci.device and ci.device != request.auth:
|
||||
return Response({
|
||||
"non_field_errors": ["Annulment is only allowed from the same device"]
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
ci.successful = False
|
||||
ci.error_reason = Checkin.REASON_ANNULLED
|
||||
ci.error_explanation = s.validated_data.get("error_explanation")
|
||||
ci.save(update_fields=["successful", "error_reason", "error_explanation"])
|
||||
ci.position.order.log_action('pretix.event.checkin.annulled', data={
|
||||
'checkin': ci.pk,
|
||||
'position': ci.position.id,
|
||||
'positionid': ci.position.positionid,
|
||||
'datetime': annulment_time,
|
||||
'error_explanation': s.validated_data.get("error_explanation"),
|
||||
'type': ci.type,
|
||||
'list': ci.list_id,
|
||||
}, user=request.user, auth=request.auth)
|
||||
checkin_annulled.send(ci.position.order.event, checkin=ci)
|
||||
|
||||
return Response({"status": "ok"}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -943,6 +943,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
@action(detail=True, methods=['POST'])
|
||||
def change(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
check_quotas = self.request.query_params.get('check_quotas', 'true') == 'true'
|
||||
|
||||
serializer = OrderChangeOperationSerializer(
|
||||
context={'order': order, **self.get_serializer_context()},
|
||||
@@ -1008,7 +1009,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
elif serializer.validated_data.get('recalculate_taxes') == 'keep_gross':
|
||||
ocm.recalculate_taxes(keep='gross')
|
||||
|
||||
ocm.commit()
|
||||
ocm.commit(check_quotas=check_quotas)
|
||||
except OrderError as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
@@ -1087,6 +1088,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
|
||||
ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
|
||||
return ctx
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@@ -48,7 +48,7 @@ from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import (
|
||||
BaseDocTemplate, Flowable, Frame, KeepTogether, NextPageTemplate,
|
||||
PageTemplate, Paragraph, Spacer, Table, TableStyle,
|
||||
PageTemplate, Spacer, Table, TableStyle,
|
||||
)
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
@@ -56,7 +56,9 @@ from pretix.base.models import Event, Invoice, Order, OrderPayment
|
||||
from pretix.base.services.currencies import SOURCE_NAMES
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
|
||||
from pretix.helpers.reportlab import (
|
||||
FontFallbackParagraph, ThumbnailingImageReader, reshaper,
|
||||
)
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -235,16 +237,17 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
italic='OpenSansIt', boldItalic='OpenSansBI')
|
||||
|
||||
for family, styles in get_fonts(event=self.event, pdf_support_required=True).items():
|
||||
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
||||
if family == self.event.settings.invoice_renderer_font:
|
||||
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
||||
self.font_regular = family
|
||||
if 'italic' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
|
||||
if 'bold' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
|
||||
self.font_bold = family + ' B'
|
||||
if 'bolditalic' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
||||
if 'italic' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
|
||||
if 'bold' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
|
||||
if 'bolditalic' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
||||
|
||||
def _normalize(self, text):
|
||||
# reportlab does not support unicode combination characters
|
||||
@@ -393,8 +396,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
invoice_to_top = 52 * mm
|
||||
|
||||
def _draw_invoice_to(self, canvas):
|
||||
p = Paragraph(self._clean_text(self.invoice.address_invoice_to),
|
||||
style=self.stylesheet['Normal'])
|
||||
p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to),
|
||||
style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
||||
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
||||
@@ -405,7 +408,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
invoice_from_top = 17 * mm
|
||||
|
||||
def _draw_invoice_from(self, canvas):
|
||||
p = Paragraph(
|
||||
p = FontFallbackParagraph(
|
||||
self._clean_text(self.invoice.full_invoice_from),
|
||||
style=self.stylesheet['InvoiceFrom']
|
||||
)
|
||||
@@ -523,12 +526,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
def shorten(txt):
|
||||
txt = str(txt)
|
||||
txt = bleach.clean(txt, tags=set()).strip()
|
||||
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
|
||||
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
||||
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
||||
p = Paragraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
return txt
|
||||
|
||||
@@ -554,7 +557,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
else:
|
||||
p_str = shorten(self.invoice.event.name)
|
||||
|
||||
p = Paragraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||
p = FontFallbackParagraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, self.event_width, self.event_height)
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
|
||||
@@ -608,7 +611,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
def _get_intro(self):
|
||||
story = []
|
||||
if self.invoice.custom_field:
|
||||
story.append(Paragraph(
|
||||
story.append(FontFallbackParagraph(
|
||||
'{}: {}'.format(
|
||||
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)),
|
||||
self._clean_text(self.invoice.custom_field),
|
||||
@@ -617,7 +620,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
))
|
||||
|
||||
if self.invoice.internal_reference:
|
||||
story.append(Paragraph(
|
||||
story.append(FontFallbackParagraph(
|
||||
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
|
||||
reference=self._clean_text(self.invoice.internal_reference),
|
||||
)),
|
||||
@@ -625,14 +628,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
))
|
||||
|
||||
if self.invoice.invoice_to_vat_id:
|
||||
story.append(Paragraph(
|
||||
story.append(FontFallbackParagraph(
|
||||
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
|
||||
self._clean_text(self.invoice.invoice_to_vat_id),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.invoice_to_beneficiary:
|
||||
story.append(Paragraph(
|
||||
story.append(FontFallbackParagraph(
|
||||
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
|
||||
self._clean_text(self.invoice.invoice_to_beneficiary),
|
||||
self.stylesheet['Normal']
|
||||
@@ -644,7 +647,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
if story:
|
||||
story.append(Spacer(1, 5 * mm))
|
||||
|
||||
story.append(Paragraph(
|
||||
story.append(FontFallbackParagraph(
|
||||
self._clean_text(self.invoice.introductory_text, tags=['br']),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
@@ -657,7 +660,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
story = [
|
||||
NextPageTemplate('FirstPage'),
|
||||
Paragraph(
|
||||
FontFallbackParagraph(
|
||||
self._normalize(
|
||||
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
||||
else pgettext('invoice', 'Invoice')
|
||||
@@ -683,17 +686,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
]
|
||||
if has_taxes:
|
||||
tdata = [(
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
|
||||
)]
|
||||
else:
|
||||
tdata = [(
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
|
||||
)]
|
||||
|
||||
def _group_key(line):
|
||||
@@ -715,14 +718,20 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
)
|
||||
description = description + "\n" + single_price_line
|
||||
tdata.append((
|
||||
Paragraph(
|
||||
FontFallbackParagraph(
|
||||
self._clean_text(description, tags=['br']),
|
||||
self.stylesheet['Normal']
|
||||
),
|
||||
str(len(lines)),
|
||||
localize(tax_rate) + " %",
|
||||
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']),
|
||||
FontFallbackParagraph(
|
||||
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||
self.stylesheet['NormalRight']
|
||||
),
|
||||
FontFallbackParagraph(
|
||||
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||
self.stylesheet['NormalRight']
|
||||
),
|
||||
))
|
||||
else:
|
||||
if len(lines) > 1:
|
||||
@@ -731,12 +740,15 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
)
|
||||
description = description + "\n" + single_price_line
|
||||
tdata.append((
|
||||
Paragraph(
|
||||
FontFallbackParagraph(
|
||||
self._clean_text(description, tags=['br']),
|
||||
self.stylesheet['Normal']
|
||||
),
|
||||
str(len(lines)),
|
||||
Paragraph(money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), self.stylesheet['NormalRight']),
|
||||
FontFallbackParagraph(
|
||||
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)
|
||||
@@ -744,13 +756,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if has_taxes:
|
||||
tdata.append([
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
|
||||
money_filter(total, self.invoice.event.currency)
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
|
||||
else:
|
||||
tdata.append([
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
|
||||
money_filter(total, self.invoice.event.currency)
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.65, .20, .15)]
|
||||
@@ -760,12 +772,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
pending_sum = self.invoice.order.pending_sum
|
||||
if pending_sum != total:
|
||||
tdata.append(
|
||||
[Paragraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
|
||||
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
|
||||
(['', '', ''] if has_taxes else ['']) +
|
||||
[money_filter(pending_sum - total, self.invoice.event.currency)]
|
||||
)
|
||||
tdata.append(
|
||||
[Paragraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
|
||||
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
|
||||
(['', '', ''] if has_taxes else ['']) +
|
||||
[money_filter(pending_sum, self.invoice.event.currency)]
|
||||
)
|
||||
@@ -782,12 +794,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
s=Sum('amount')
|
||||
)['s'] or Decimal('0.00')
|
||||
tdata.append(
|
||||
[Paragraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
|
||||
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
|
||||
(['', '', ''] if has_taxes else ['']) +
|
||||
[money_filter(giftcard_sum, self.invoice.event.currency)]
|
||||
)
|
||||
tdata.append(
|
||||
[Paragraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
|
||||
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
|
||||
(['', '', ''] if has_taxes else ['']) +
|
||||
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
|
||||
)
|
||||
@@ -810,7 +822,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
story.append(Spacer(1, 10 * mm))
|
||||
|
||||
if self.invoice.payment_provider_text:
|
||||
story.append(Paragraph(
|
||||
story.append(FontFallbackParagraph(
|
||||
self._normalize(self.invoice.payment_provider_text),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
@@ -819,7 +831,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
story.append(Spacer(1, 3 * mm))
|
||||
|
||||
if self.invoice.additional_text:
|
||||
story.append(Paragraph(
|
||||
story.append(FontFallbackParagraph(
|
||||
self._clean_text(self.invoice.additional_text, tags=['br']),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
@@ -835,10 +847,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
||||
]
|
||||
thead = [
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
|
||||
''
|
||||
]
|
||||
tdata = [thead]
|
||||
@@ -849,7 +861,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
continue
|
||||
tax = taxvalue_map[idx]
|
||||
tdata.append([
|
||||
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||
money_filter(gross - tax, self.invoice.event.currency),
|
||||
money_filter(gross, self.invoice.event.currency),
|
||||
money_filter(tax, self.invoice.event.currency),
|
||||
@@ -868,7 +880,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
table.setStyle(TableStyle(tstyledata))
|
||||
story.append(Spacer(5 * mm, 5 * mm))
|
||||
story.append(KeepTogether([
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
|
||||
table
|
||||
]))
|
||||
|
||||
@@ -885,7 +897,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
net = gross - tax
|
||||
|
||||
tdata.append([
|
||||
Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||
fmt(net), fmt(gross), fmt(tax), ''
|
||||
])
|
||||
|
||||
@@ -894,7 +906,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
story.append(KeepTogether([
|
||||
Spacer(1, height=2 * mm),
|
||||
Paragraph(
|
||||
FontFallbackParagraph(
|
||||
self._normalize(pgettext(
|
||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||
'{date}, this corresponds to:'
|
||||
@@ -909,7 +921,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
||||
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
|
||||
story.append(Spacer(1, 5 * mm))
|
||||
story.append(Paragraph(self._normalize(
|
||||
story.append(FontFallbackParagraph(self._normalize(
|
||||
pgettext(
|
||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||
'{date}, the invoice total corresponds to {total}.'
|
||||
@@ -962,7 +974,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
self._clean_text(l)
|
||||
for l in self.invoice.address_invoice_from.strip().split('\n')
|
||||
]
|
||||
p = Paragraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
|
||||
p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
|
||||
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
|
||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
|
||||
super()._draw_invoice_from(canvas)
|
||||
@@ -1021,7 +1033,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
|
||||
]
|
||||
|
||||
p = Paragraph(
|
||||
p = FontFallbackParagraph(
|
||||
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
|
||||
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
|
||||
)
|
||||
@@ -1079,7 +1091,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
|
||||
i = []
|
||||
|
||||
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
|
||||
i.append(Paragraph(
|
||||
i.append(FontFallbackParagraph(
|
||||
pgettext('invoice', 'Event date: {date_range}').format(
|
||||
date_range=self.invoice.event.get_date_range_display(),
|
||||
),
|
||||
|
||||
46
src/pretix/base/migrations/0285_voucher_created.py
Normal file
46
src/pretix/base/migrations/0285_voucher_created.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 4.2.16 on 2025-08-08 09:13
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db.models import Min
|
||||
from django.utils.timezone import now
|
||||
|
||||
|
||||
def backfill_voucher_created(apps, schema_editor):
|
||||
Voucher = apps.get_model("pretixbase", "Voucher")
|
||||
LogEntry = apps.get_model("pretixbase", "LogEntry")
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
ct = None
|
||||
|
||||
for v in Voucher.objects.filter(created__isnull=True).iterator():
|
||||
if not ct:
|
||||
# "Lazy-loading" to prevent this to be executed on new DBs where the content type does not yet
|
||||
# exist -- but also no vouchers do
|
||||
ct = ContentType.objects.get(app_label='pretixbase', model='voucher')
|
||||
v.created = LogEntry.objects.filter(
|
||||
content_type=ct,
|
||||
object_id=v.pk,
|
||||
).aggregate(m=Min("datetime"))["m"] or now()
|
||||
v.save(update_fields=["created"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0284_ordersyncresult_ordersyncqueue"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="voucher",
|
||||
name="created",
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
backfill_voucher_created,
|
||||
migrations.RunPython.noop,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="voucher",
|
||||
name="created",
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
]
|
||||
28
src/pretix/base/migrations/0286_settingsstore_unique.py
Normal file
28
src/pretix/base/migrations/0286_settingsstore_unique.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 4.2.16 on 2025-08-14 09:40
|
||||
|
||||
from django.db import migrations
|
||||
from hierarkey.utils import CleanHierarkeyDuplicates
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0285_voucher_created"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
CleanHierarkeyDuplicates("GlobalSettingsObject_SettingsStore"),
|
||||
CleanHierarkeyDuplicates("Organizer_SettingsStore"),
|
||||
CleanHierarkeyDuplicates("Event_SettingsStore"),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="event_settingsstore",
|
||||
unique_together={("object", "key")},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="globalsettingsobject_settingsstore",
|
||||
unique_together={("key",)},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="organizer_settingsstore",
|
||||
unique_together={("object", "key")},
|
||||
),
|
||||
]
|
||||
@@ -350,6 +350,7 @@ class Checkin(models.Model):
|
||||
REASON_BLOCKED = 'blocked'
|
||||
REASON_UNAPPROVED = 'unapproved'
|
||||
REASON_INVALID_TIME = 'invalid_time'
|
||||
REASON_ANNULLED = 'annulled'
|
||||
REASONS = (
|
||||
(REASON_CANCELED, _('Order canceled')),
|
||||
(REASON_INVALID, _('Unknown ticket')),
|
||||
@@ -364,6 +365,7 @@ class Checkin(models.Model):
|
||||
(REASON_BLOCKED, _('Ticket blocked')),
|
||||
(REASON_UNAPPROVED, _('Order not approved')),
|
||||
(REASON_INVALID_TIME, _('Ticket not valid at this time')),
|
||||
(REASON_ANNULLED, _('Check-in annulled')),
|
||||
)
|
||||
|
||||
successful = models.BooleanField(
|
||||
|
||||
@@ -1085,7 +1085,7 @@ class Event(EventMixin, LoggedModel):
|
||||
s.save(force_insert=True)
|
||||
|
||||
valid_sales_channel_identifers = set(self.organizer.sales_channels.values_list("identifier", flat=True))
|
||||
skip_settings = (
|
||||
skip_settings = {
|
||||
'ticket_secrets_pretix_sig1_pubkey',
|
||||
'ticket_secrets_pretix_sig1_privkey',
|
||||
# no longer used, but we still don't need to copy them
|
||||
@@ -1093,7 +1093,10 @@ class Event(EventMixin, LoggedModel):
|
||||
'presale_css_checksum',
|
||||
'presale_widget_css_file',
|
||||
'presale_widget_css_checksum',
|
||||
)
|
||||
} | {
|
||||
# Some settings might already exist due to e.g. the timezone being special in the API
|
||||
s.key for s in self.settings._objects.all()
|
||||
}
|
||||
settings_to_save = []
|
||||
for s in other.settings._objects.all():
|
||||
if s.key in skip_settings:
|
||||
|
||||
@@ -3314,6 +3314,24 @@ class InvoiceAddress(models.Model):
|
||||
kwargs['update_fields'] = {'name_cached', 'name_parts'}.union(kwargs['update_fields'])
|
||||
super().save(**kwargs)
|
||||
|
||||
def clear(self, except_name=False):
|
||||
self.is_business = False
|
||||
if not except_name:
|
||||
self.name_cached = ""
|
||||
self.name_parts = {}
|
||||
self.company = ""
|
||||
self.street = ""
|
||||
self.zipcode = ""
|
||||
self.city = ""
|
||||
self.country_old = ""
|
||||
self.country = ""
|
||||
self.state = ""
|
||||
self.vat_id = ""
|
||||
self.vat_id_validated = False
|
||||
self.custom_field = None
|
||||
self.internal_reference = ""
|
||||
self.beneficiary = ""
|
||||
|
||||
def describe(self):
|
||||
parts = [
|
||||
self.company,
|
||||
|
||||
@@ -174,6 +174,9 @@ class Voucher(LoggedModel):
|
||||
('percent', _('Reduce product price by (%)')),
|
||||
)
|
||||
|
||||
created = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
)
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
on_delete=models.CASCADE,
|
||||
|
||||
@@ -668,6 +668,16 @@ For backwards compatibility reasons, this signal is only sent when a **successfu
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
checkin_annulled = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``checkin``
|
||||
|
||||
This signal is sent out every time a check-in is annulled (i.e. changed to unsuccessful after it
|
||||
already was successful).
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
logentry_display = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``logentry``
|
||||
|
||||
@@ -321,6 +321,14 @@ class OrderChangedSplitFrom(OrderLogEntryType):
|
||||
_('Denied scan of position #{posid} at {datetime} for list "{list}", type "{type}", error code "{errorcode}".'),
|
||||
_('Denied scan of position #{posid} for list "{list}", type "{type}", error code "{errorcode}".'),
|
||||
),
|
||||
'pretix.event.checkin.annulled': (
|
||||
_('Annulled scan of position #{posid} at {datetime} for list "{list}", type "{type}".'),
|
||||
_('Annulled scan of position #{posid} for list "{list}", type "{type}".'),
|
||||
),
|
||||
'pretix.event.checkin.annulment.ignored': (
|
||||
_('Ignored annulment of position #{posid} at {datetime} for list "{list}", type "{type}".'),
|
||||
_('Ignored annulment of position #{posid} for list "{list}", type "{type}".'),
|
||||
),
|
||||
'pretix.control.views.checkin.reverted': _('The check-in of position #{posid} on list "{list}" has been reverted.'),
|
||||
'pretix.event.checkin.reverted': _('The check-in of position #{posid} on list "{list}" has been reverted.'),
|
||||
})
|
||||
|
||||
@@ -19,11 +19,20 @@
|
||||
# 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/>.
|
||||
#
|
||||
import logging
|
||||
|
||||
from arabic_reshaper import ArabicReshaper
|
||||
from django.conf import settings
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from PIL import Image
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.platypus import Paragraph
|
||||
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThumbnailingImageReader(ImageReader):
|
||||
@@ -59,3 +68,35 @@ reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
||||
'delete_harakat': True,
|
||||
'support_ligatures': False,
|
||||
}))
|
||||
|
||||
|
||||
class FontFallbackParagraph(Paragraph):
|
||||
def __init__(self, text, style=None, *args, **kwargs):
|
||||
if style is None:
|
||||
style = ParagraphStyle(name='paragraphImplicitDefaultStyle')
|
||||
|
||||
if not self._font_supports_text(text, style.fontName):
|
||||
newFont = self._find_font(text, style.fontName)
|
||||
if newFont:
|
||||
logger.debug(f"replacing {style.fontName} with {newFont} for {text!r}")
|
||||
style = style.clone(name=style.name + '_' + newFont, fontName=newFont)
|
||||
|
||||
super().__init__(text, style, *args, **kwargs)
|
||||
|
||||
def _font_supports_text(self, text, font_name):
|
||||
if not text:
|
||||
return True
|
||||
font = pdfmetrics.getFont(font_name)
|
||||
return all(
|
||||
ord(c) in font.face.charToGlyph or not c.isprintable()
|
||||
for c in text
|
||||
)
|
||||
|
||||
def _find_font(self, text, original_font):
|
||||
for family, styles in get_fonts(pdf_support_required=True).items():
|
||||
if self._font_supports_text(text, family):
|
||||
if (original_font.endswith("It") or original_font.endswith(" I")) and "italic" in styles:
|
||||
return family + " I"
|
||||
if (original_font.endswith("Bd") or original_font.endswith(" B")) and "bold" in styles:
|
||||
return family + " B"
|
||||
return family
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-08-05 07:29+0000\n"
|
||||
"PO-Revision-Date: 2025-08-05 20:00+0000\n"
|
||||
"Last-Translator: Ryo Tagami <rtagami@airstrip.jp>\n"
|
||||
"PO-Revision-Date: 2025-08-08 06:00+0000\n"
|
||||
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ja/>\n"
|
||||
"Language: ja\n"
|
||||
@@ -16949,7 +16949,7 @@ msgstr "税金のルール"
|
||||
|
||||
#: pretix/control/navigation.py:97
|
||||
msgid "Invoicing"
|
||||
msgstr "請求書を作成します"
|
||||
msgstr "請求書作成"
|
||||
|
||||
#: pretix/control/navigation.py:105
|
||||
msgctxt "action"
|
||||
|
||||
@@ -49,7 +49,7 @@ from django.utils.translation import (
|
||||
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
|
||||
)
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.platypus import Flowable, Paragraph, Spacer, Table, TableStyle
|
||||
from reportlab.platypus import Flowable, Spacer, Table, TableStyle
|
||||
|
||||
from pretix.base.exporter import BaseExporter, ListExporter
|
||||
from pretix.base.models import (
|
||||
@@ -64,6 +64,7 @@ from pretix.base.timeframes import (
|
||||
from pretix.control.forms.widgets import Select2
|
||||
from pretix.helpers.filenames import safe_for_filename
|
||||
from pretix.helpers.iter import chunked_iterable
|
||||
from pretix.helpers.reportlab import FontFallbackParagraph
|
||||
from pretix.helpers.templatetags.jsonfield import JSONExtract
|
||||
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
||||
|
||||
@@ -343,7 +344,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
|
||||
]
|
||||
|
||||
story = [
|
||||
Paragraph(
|
||||
FontFallbackParagraph(
|
||||
cl.name,
|
||||
headlinestyle
|
||||
),
|
||||
@@ -351,7 +352,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
|
||||
if cl.subevent:
|
||||
story += [
|
||||
Spacer(1, 3 * mm),
|
||||
Paragraph(
|
||||
FontFallbackParagraph(
|
||||
'{} ({} {})'.format(
|
||||
cl.subevent.name,
|
||||
cl.subevent.get_date_range_display(),
|
||||
@@ -381,10 +382,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
|
||||
headrowstyle.fontName = 'OpenSansBd'
|
||||
for q in questions:
|
||||
txt = str(q.question)
|
||||
p = Paragraph(txt, headrowstyle)
|
||||
p = FontFallbackParagraph(txt, headrowstyle)
|
||||
while p.wrap(colwidths[len(tdata[0])], 5000)[1] > 30 * mm:
|
||||
txt = txt[:len(txt) - 50] + "..."
|
||||
p = Paragraph(txt, headrowstyle)
|
||||
p = FontFallbackParagraph(txt, headrowstyle)
|
||||
tdata[0].append(p)
|
||||
|
||||
qs = self._get_queryset(cl, form_data)
|
||||
@@ -431,8 +432,8 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
|
||||
CBFlowable(bool(op.last_checked_in)) if not op.blocked else '—',
|
||||
'✘' if op.order.status != Order.STATUS_PAID else '✔',
|
||||
op.order.code,
|
||||
Paragraph(name, self.get_style()),
|
||||
Paragraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()),
|
||||
FontFallbackParagraph(name, self.get_style()),
|
||||
FontFallbackParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()),
|
||||
]
|
||||
acache = {}
|
||||
if op.addon_to:
|
||||
@@ -443,10 +444,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
|
||||
for q in questions:
|
||||
txt = acache.get(q.pk, '')
|
||||
txt = bleach.clean(txt, tags={'br'}).strip().replace('<br>', '<br/>')
|
||||
p = Paragraph(txt, self.get_style())
|
||||
p = FontFallbackParagraph(txt, self.get_style())
|
||||
while p.wrap(colwidths[len(row)], 5000)[1] > 50 * mm:
|
||||
txt = txt[:len(txt) - 50] + "..."
|
||||
p = Paragraph(txt, self.get_style())
|
||||
p = FontFallbackParagraph(txt, self.get_style())
|
||||
row.append(p)
|
||||
if op.order.status != Order.STATUS_PAID:
|
||||
tstyledata += [
|
||||
|
||||
@@ -49,6 +49,7 @@ from pretix.base.timeframes import (
|
||||
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
|
||||
)
|
||||
from pretix.control.forms.filter import get_all_payment_providers
|
||||
from pretix.helpers.reportlab import FontFallbackParagraph
|
||||
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
||||
|
||||
|
||||
@@ -310,13 +311,13 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
|
||||
tdata = [
|
||||
[
|
||||
Paragraph(self._transaction_group_header_label(), tstyle_bold),
|
||||
Paragraph(_("Price"), tstyle_bold_right),
|
||||
Paragraph(_("Tax rate"), tstyle_bold_right),
|
||||
Paragraph("#", tstyle_bold_right),
|
||||
Paragraph(_("Net total"), tstyle_bold_right),
|
||||
Paragraph(_("Tax total"), tstyle_bold_right),
|
||||
Paragraph(_("Gross total"), tstyle_bold_right),
|
||||
FontFallbackParagraph(self._transaction_group_header_label(), tstyle_bold),
|
||||
FontFallbackParagraph(_("Price"), tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Tax rate"), tstyle_bold_right),
|
||||
FontFallbackParagraph("#", tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Net total"), tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Tax total"), tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Gross total"), tstyle_bold_right),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -351,7 +352,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
|
||||
tdata.append(
|
||||
[
|
||||
Paragraph(
|
||||
FontFallbackParagraph(
|
||||
e,
|
||||
tstyle_bold,
|
||||
),
|
||||
@@ -374,7 +375,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
text = self._transaction_row_label(r)
|
||||
tdata.append(
|
||||
[
|
||||
Paragraph(text, tstyle),
|
||||
FontFallbackParagraph(text, tstyle),
|
||||
Paragraph(
|
||||
money_filter(r["price"], currency)
|
||||
if "price" in r and r["price"] is not None
|
||||
@@ -405,7 +406,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
for tax_rate in sorted(sum_tax_by_tax_rate.keys(), reverse=True):
|
||||
tdata.append(
|
||||
[
|
||||
Paragraph(_("Sum"), tstyle),
|
||||
FontFallbackParagraph(_("Sum"), tstyle),
|
||||
Paragraph("", tstyle_right),
|
||||
Paragraph(localize(tax_rate.normalize()) + " %", tstyle_right),
|
||||
Paragraph("", tstyle_right),
|
||||
@@ -438,7 +439,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
|
||||
tdata.append(
|
||||
[
|
||||
Paragraph(_("Sum"), tstyle_bold),
|
||||
FontFallbackParagraph(_("Sum"), tstyle_bold),
|
||||
Paragraph("", tstyle_right),
|
||||
Paragraph("", tstyle_right),
|
||||
Paragraph("", tstyle_bold_right),
|
||||
@@ -492,10 +493,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
|
||||
tdata = [
|
||||
[
|
||||
Paragraph(_("Payment method"), tstyle_bold),
|
||||
Paragraph(_("Payments"), tstyle_bold_right),
|
||||
Paragraph(_("Refunds"), tstyle_bold_right),
|
||||
Paragraph(_("Total"), tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Payment method"), tstyle_bold),
|
||||
FontFallbackParagraph(_("Payments"), tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Refunds"), tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Total"), tstyle_bold_right),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -537,7 +538,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
tdata.append(
|
||||
[
|
||||
Paragraph(provider_names.get(p, p), tstyle),
|
||||
Paragraph(
|
||||
FontFallbackParagraph(
|
||||
money_filter(payments_by_provider[p], currency)
|
||||
if p in payments_by_provider
|
||||
else "",
|
||||
@@ -562,7 +563,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
|
||||
tdata.append(
|
||||
[
|
||||
Paragraph(_("Sum"), tstyle_bold),
|
||||
FontFallbackParagraph(_("Sum"), tstyle_bold),
|
||||
Paragraph(
|
||||
money_filter(
|
||||
sum(payments_by_provider.values(), Decimal("0.00")), currency
|
||||
@@ -640,7 +641,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
open_before = tx_before - p_before + r_before
|
||||
tdata.append(
|
||||
[
|
||||
Paragraph(
|
||||
FontFallbackParagraph(
|
||||
_("Pending payments at {datetime}").format(
|
||||
datetime=date_format(
|
||||
df_start - datetime.timedelta.resolution,
|
||||
@@ -667,21 +668,21 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
] or Decimal("0.00")
|
||||
tdata.append(
|
||||
[
|
||||
Paragraph(_("Orders"), tstyle),
|
||||
FontFallbackParagraph(_("Orders"), tstyle),
|
||||
Paragraph("+", tstyle_center),
|
||||
Paragraph(money_filter(tx_during, currency), tstyle_right),
|
||||
]
|
||||
)
|
||||
tdata.append(
|
||||
[
|
||||
Paragraph(_("Payments"), tstyle),
|
||||
FontFallbackParagraph(_("Payments"), tstyle),
|
||||
Paragraph("-", tstyle_center),
|
||||
Paragraph(money_filter(p_during, currency), tstyle_right),
|
||||
]
|
||||
)
|
||||
tdata.append(
|
||||
[
|
||||
Paragraph(_("Refunds"), tstyle),
|
||||
FontFallbackParagraph(_("Refunds"), tstyle),
|
||||
Paragraph("+", tstyle_center),
|
||||
Paragraph(money_filter(r_during, currency), tstyle_right),
|
||||
]
|
||||
@@ -767,7 +768,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
] or Decimal("0.00")
|
||||
tdata.append(
|
||||
[
|
||||
Paragraph(_("Gift card transactions (credit)"), tstyle),
|
||||
FontFallbackParagraph(_("Gift card transactions (credit)"), tstyle),
|
||||
Paragraph(money_filter(tx_during_pos, currency), tstyle_right),
|
||||
]
|
||||
)
|
||||
@@ -777,7 +778,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
] or Decimal("0.00")
|
||||
tdata.append(
|
||||
[
|
||||
Paragraph(_("Gift card transactions (debit)"), tstyle),
|
||||
FontFallbackParagraph(_("Gift card transactions (debit)"), tstyle),
|
||||
Paragraph(money_filter(tx_during_neg, currency), tstyle_right),
|
||||
]
|
||||
)
|
||||
@@ -845,9 +846,9 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
style_small.leading = 10
|
||||
|
||||
story = [
|
||||
Paragraph(self.verbose_name, style_h1),
|
||||
FontFallbackParagraph(self.verbose_name, style_h1),
|
||||
Spacer(0, 3 * mm),
|
||||
Paragraph(
|
||||
FontFallbackParagraph(
|
||||
"<br />".join(escape(f) for f in self.describe_filters(form_data)),
|
||||
style_small,
|
||||
),
|
||||
@@ -859,7 +860,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
c_head = f" [{c}]" if len(currencies) > 1 else ""
|
||||
story += [
|
||||
Spacer(0, 3 * mm),
|
||||
Paragraph(_("Orders") + c_head, style_h2),
|
||||
FontFallbackParagraph(_("Orders") + c_head, style_h2),
|
||||
Spacer(0, 3 * mm),
|
||||
*self._table_transactions(form_data, c),
|
||||
]
|
||||
@@ -868,7 +869,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
c_head = f" [{c}]" if len(currencies) > 1 else ""
|
||||
story += [
|
||||
Spacer(0, 8 * mm),
|
||||
Paragraph(_("Payments") + c_head, style_h2),
|
||||
FontFallbackParagraph(_("Payments") + c_head, style_h2),
|
||||
Spacer(0, 3 * mm),
|
||||
*self._table_payments(form_data, c),
|
||||
]
|
||||
@@ -879,7 +880,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
Spacer(0, 8 * mm),
|
||||
KeepTogether(
|
||||
[
|
||||
Paragraph(_("Open items") + c_head, style_h2),
|
||||
FontFallbackParagraph(_("Open items") + c_head, style_h2),
|
||||
Spacer(0, 3 * mm),
|
||||
*self._table_open_items(form_data, c),
|
||||
]
|
||||
@@ -895,7 +896,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
Spacer(0, 8 * mm),
|
||||
KeepTogether(
|
||||
[
|
||||
Paragraph(_("Gift cards") + c_head, style_h2),
|
||||
FontFallbackParagraph(_("Gift cards") + c_head, style_h2),
|
||||
Spacer(0, 3 * mm),
|
||||
*self._table_gift_cards(form_data, c),
|
||||
]
|
||||
|
||||
@@ -56,7 +56,7 @@ from reportlab.lib import colors
|
||||
from reportlab.lib.enums import TA_CENTER
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import PageBreak, Paragraph, Spacer, Table, TableStyle
|
||||
from reportlab.platypus import PageBreak, Spacer, Table, TableStyle
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.exporter import BaseExporter, MultiSheetListExporter
|
||||
@@ -69,6 +69,8 @@ from pretix.base.timeframes import (
|
||||
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
|
||||
)
|
||||
from pretix.control.forms.filter import OverviewFilterForm
|
||||
from pretix.helpers.reportlab import FontFallbackParagraph
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
|
||||
class NumberedCanvas(Canvas):
|
||||
@@ -135,6 +137,15 @@ class ReportlabExportMixin:
|
||||
pdfmetrics.registerFont(TTFont('OpenSansIt', finders.find('fonts/OpenSans-Italic.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('OpenSansBd', finders.find('fonts/OpenSans-Bold.ttf')))
|
||||
|
||||
for family, styles in get_fonts(None, pdf_support_required=True).items():
|
||||
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
||||
if 'italic' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
|
||||
if 'bold' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
|
||||
if 'bolditalic' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
||||
|
||||
def get_doc_template(self):
|
||||
from reportlab.platypus import BaseDocTemplate
|
||||
|
||||
@@ -272,7 +283,7 @@ class OverviewReport(Report):
|
||||
headlinestyle.fontSize = 15
|
||||
headlinestyle.fontName = 'OpenSansBd'
|
||||
story = [
|
||||
Paragraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle),
|
||||
FontFallbackParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle),
|
||||
Spacer(1, 5 * mm)
|
||||
]
|
||||
return story
|
||||
@@ -282,7 +293,7 @@ class OverviewReport(Report):
|
||||
if form_data.get('date_axis') and form_data.get('date_range'):
|
||||
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
|
||||
story += [
|
||||
Paragraph(_('{axis} between {start} and {end}').format(
|
||||
FontFallbackParagraph(_('{axis} between {start} and {end}').format(
|
||||
axis=dict(OverviewFilterForm(event=self.event).fields['date_axis'].choices)[form_data.get('date_axis')],
|
||||
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '–',
|
||||
end=date_format(d_end, 'SHORT_DATE_FORMAT') if d_end else '–',
|
||||
@@ -295,13 +306,13 @@ class OverviewReport(Report):
|
||||
subevent = self.event.subevents.get(pk=self.form_data.get('subevent'))
|
||||
except SubEvent.DoesNotExist:
|
||||
subevent = self.form_data.get('subevent')
|
||||
story.append(Paragraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style()))
|
||||
story.append(FontFallbackParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style()))
|
||||
story.append(Spacer(1, 5 * mm))
|
||||
|
||||
if form_data.get('subevent_date_range'):
|
||||
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['subevent_date_range'], self.timezone)
|
||||
story += [
|
||||
Paragraph(_('{axis} between {start} and {end}').format(
|
||||
FontFallbackParagraph(_('{axis} between {start} and {end}').format(
|
||||
axis=_('Event date'),
|
||||
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '–',
|
||||
end=date_format(d_end - timedelta(hours=1), 'SHORT_DATE_FORMAT') if d_end else '–',
|
||||
@@ -373,13 +384,13 @@ class OverviewReport(Report):
|
||||
tdata = [
|
||||
[
|
||||
_('Product'),
|
||||
Paragraph(_('Canceled'), tstyle_th),
|
||||
FontFallbackParagraph(_('Canceled'), tstyle_th),
|
||||
'',
|
||||
Paragraph(_('Expired'), tstyle_th),
|
||||
FontFallbackParagraph(_('Expired'), tstyle_th),
|
||||
'',
|
||||
Paragraph(_('Approval pending'), tstyle_th),
|
||||
FontFallbackParagraph(_('Approval pending'), tstyle_th),
|
||||
'',
|
||||
Paragraph(_('Purchased'), tstyle_th),
|
||||
FontFallbackParagraph(_('Purchased'), tstyle_th),
|
||||
'', '', '', '', ''
|
||||
],
|
||||
[
|
||||
@@ -410,14 +421,14 @@ class OverviewReport(Report):
|
||||
for tup in items_by_category:
|
||||
if tup[0]:
|
||||
tdata.append([
|
||||
Paragraph(str(tup[0]), tstyle_bold)
|
||||
FontFallbackParagraph(str(tup[0]), tstyle_bold)
|
||||
])
|
||||
for l, s in states:
|
||||
tdata[-1].append(str(tup[0].num[l][0]))
|
||||
tdata[-1].append(floatformat(tup[0].num[l][2 if net else 1], places))
|
||||
for item in tup[1]:
|
||||
tdata.append([
|
||||
Paragraph(str(item), tstyle)
|
||||
FontFallbackParagraph(str(item), tstyle)
|
||||
])
|
||||
for l, s in states:
|
||||
tdata[-1].append(str(item.num[l][0]))
|
||||
@@ -425,7 +436,7 @@ class OverviewReport(Report):
|
||||
if item.has_variations:
|
||||
for var in item.all_variations:
|
||||
tdata.append([
|
||||
Paragraph(" " + str(var), tstyle)
|
||||
FontFallbackParagraph(" " + str(var), tstyle)
|
||||
])
|
||||
for l, s in states:
|
||||
tdata[-1].append(str(var.num[l][0]))
|
||||
@@ -512,7 +523,7 @@ class OrderTaxListReportPDF(Report):
|
||||
|
||||
def get_story(self, doc, form_data):
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.platypus import Paragraph, Spacer, Table, TableStyle
|
||||
from reportlab.platypus import Spacer, Table, TableStyle
|
||||
|
||||
headlinestyle = self.get_style()
|
||||
headlinestyle.fontSize = 15
|
||||
@@ -553,7 +564,7 @@ class OrderTaxListReportPDF(Report):
|
||||
tstyledata.append(('SPAN', (5 + 2 * i, 0), (6 + 2 * i, 0)))
|
||||
|
||||
story = [
|
||||
Paragraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle),
|
||||
FontFallbackParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle),
|
||||
Spacer(1, 5 * mm)
|
||||
]
|
||||
tdata = [
|
||||
|
||||
@@ -959,6 +959,10 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
d['phone'] = str(d['phone'])
|
||||
self.cart_session['contact_form_data'] = d
|
||||
if self.address_asked or self.request.event.settings.invoice_name_required:
|
||||
if not self.address_asked:
|
||||
# Invoice address was there, but is no longer asked for, however, name is still required
|
||||
self.invoice_form.instance.clear(except_name=True)
|
||||
|
||||
addr = self.invoice_form.save()
|
||||
|
||||
if self.cart_customer and self.invoice_form.cleaned_data.get('save'):
|
||||
@@ -997,6 +1001,10 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
'rate to your purchase and the price of the products in your cart has '
|
||||
'changed accordingly.'))
|
||||
return redirect_to_url(self.get_next_url(request) + '?open_cart=true')
|
||||
elif 'invoice_address' in self.cart_session:
|
||||
# Invoice address was there, but is no longer asked for
|
||||
self.invoice_address.delete()
|
||||
del self.cart_session['invoice_address']
|
||||
|
||||
try:
|
||||
validate_memberships_in_order(self.cart_customer, self.positions, self.request.event, lock=False,
|
||||
|
||||
@@ -437,7 +437,7 @@
|
||||
{% if not hide_prices %}
|
||||
<div role="rowgroup" class="cart-rowgroup-total">
|
||||
{% if event.settings.display_net_prices and cart.tax_total %}
|
||||
<div role="row" class="row cart-row">
|
||||
<div role="row" class="row cart-row subtotal">
|
||||
<div role="cell" class="product">
|
||||
<strong>{% trans "Net total" %}</strong>
|
||||
</div>
|
||||
@@ -448,7 +448,7 @@
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div role="row" class="row cart-row">
|
||||
<div role="row" class="row cart-row subtotal">
|
||||
<div role="cell" class="product">
|
||||
<strong>{% trans "Taxes" %}</strong>
|
||||
</div>
|
||||
|
||||
@@ -594,7 +594,7 @@ var form_handlers = function (el) {
|
||||
}
|
||||
}
|
||||
},
|
||||
placeholder: $(this).attr("data-placeholder") | "",
|
||||
placeholder: $(this).attr("data-placeholder") || "",
|
||||
templateResult: function (res) {
|
||||
if (!res.id) {
|
||||
return res.text;
|
||||
|
||||
@@ -241,10 +241,6 @@ function setup_basics(el) {
|
||||
|
||||
el.find(".js-only").removeClass("js-only");
|
||||
el.find(".js-hidden").hide();
|
||||
// make sure to always have a #content for skip-link to work
|
||||
if (!document.querySelector("#content")) {
|
||||
(document.querySelector('main') || document.querySelector('.page-header + *')).id = "content"
|
||||
}
|
||||
|
||||
el.find("div.collapsed").removeClass("collapsed").addClass("collapse");
|
||||
el.find(".has-error, .alert-danger").each(function () {
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
&.has-downloads.hide-prices .download-desktop {
|
||||
margin-left: 50%;
|
||||
}
|
||||
&.total .product {
|
||||
&.subtotal .product, &.total .product {
|
||||
width: 50%;
|
||||
}
|
||||
.count {
|
||||
|
||||
@@ -1077,3 +1077,97 @@ def test_reason_explanation_localization(token_client, organizer, clist, other_i
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "invalid_time"
|
||||
assert resp.data["reason_explanation"] == "Erst ab 01.01.2020 12:00 gültig."
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_annul_simple(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {
|
||||
'nonce': 'nooooonce'
|
||||
})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/checkinrpc/annul/'.format(organizer.slug), {
|
||||
'lists': [clist.pk],
|
||||
'nonce': 'nooooonce',
|
||||
'error_explanation': 'Turnstile did not turn',
|
||||
}, format='json', headers={})
|
||||
assert resp.status_code == 200
|
||||
|
||||
with scopes_disabled():
|
||||
ci = p.all_checkins.get()
|
||||
assert not ci.successful
|
||||
assert ci.error_reason == Checkin.REASON_ANNULLED
|
||||
assert ci.error_explanation == "Turnstile did not turn"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_annul_failures(device_client, team, organizer, clist, clist_event2, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = _redeem(device_client, organizer, clist, p.secret, {
|
||||
'nonce': 'nooooonce',
|
||||
'datetime': '2025-04-01T12:23:45Z',
|
||||
})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
|
||||
resp = device_client.post('/api/v1/organizers/{}/checkinrpc/annul/'.format(organizer.slug), {
|
||||
'lists': [clist.pk],
|
||||
}, format='json', headers={})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {"nonce": ["This field is required."]}
|
||||
|
||||
resp = device_client.post('/api/v1/organizers/{}/checkinrpc/annul/'.format(organizer.slug), {
|
||||
'lists': [clist_event2.pk],
|
||||
'nonce': 'nooooonce',
|
||||
'error_explanation': 'Turnstile did not turn',
|
||||
}, format='json', headers={})
|
||||
assert resp.status_code == 404
|
||||
assert resp.data == {"detail": "No check-in found based on nonce"}
|
||||
|
||||
resp = device_client.post('/api/v1/organizers/{}/checkinrpc/annul/'.format(organizer.slug), {
|
||||
'lists': [clist.pk],
|
||||
'nonce': 'notfound',
|
||||
'error_explanation': 'Turnstile did not turn',
|
||||
}, format='json', headers={})
|
||||
assert resp.status_code == 404
|
||||
assert resp.data == {"detail": "No check-in found based on nonce"}
|
||||
|
||||
with scopes_disabled():
|
||||
lcnt = order.all_logentries().count()
|
||||
|
||||
resp = device_client.post('/api/v1/organizers/{}/checkinrpc/annul/'.format(organizer.slug), {
|
||||
'lists': [clist.pk],
|
||||
'nonce': 'nooooonce',
|
||||
'error_explanation': 'Turnstile did not turn',
|
||||
}, format='json', headers={})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'non_field_errors': ['Annulment is not allowed more than 15 minutes after check-in']
|
||||
}
|
||||
|
||||
with scopes_disabled():
|
||||
assert order.all_logentries().count() == lcnt + 1
|
||||
|
||||
t = team.tokens.create(name='Foo')
|
||||
team.all_events = True
|
||||
team.save()
|
||||
device_client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
|
||||
|
||||
resp = device_client.post('/api/v1/organizers/{}/checkinrpc/annul/'.format(organizer.slug), {
|
||||
'lists': [clist.pk],
|
||||
'nonce': 'nooooonce',
|
||||
'error_explanation': 'Turnstile did not turn',
|
||||
'datetime': '2025-04-01T12:24:45Z',
|
||||
}, format='json', headers={})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'non_field_errors': ['Annulment is only allowed from the same device']
|
||||
}
|
||||
|
||||
with scopes_disabled():
|
||||
ci = p.all_checkins.get()
|
||||
assert ci.successful
|
||||
|
||||
@@ -1204,6 +1204,106 @@ def test_position_update_change_item_no_quota(token_client, organizer, event, or
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_update_change_item_empty_quota(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
q = event.quotas.create(name="No Quota", size=0)
|
||||
q.items.add(item2)
|
||||
q.save()
|
||||
op = order.positions.first()
|
||||
payload = {
|
||||
'item': item2.pk,
|
||||
}
|
||||
assert op.item != item2
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
|
||||
organizer.slug, event.slug, op.pk
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_update_change_item_no_quota_check_quota_false(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
op = order.positions.first()
|
||||
payload = {
|
||||
'item': item2.pk,
|
||||
}
|
||||
assert op.item != item2
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/orderpositions/{}/?check_quotas=false'.format(
|
||||
organizer.slug, event.slug, op.pk
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_update_change_item_empty_quota_check_quota_false(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
q = event.quotas.create(name="No Quota", size=0)
|
||||
q.items.add(item2)
|
||||
q.save()
|
||||
op = order.positions.first()
|
||||
payload = {
|
||||
'item': item2.pk,
|
||||
}
|
||||
assert op.item != item2
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/orderpositions/{}/?check_quotas=false'.format(
|
||||
organizer.slug, event.slug, op.pk
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
op.refresh_from_db()
|
||||
assert op.item == item2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_update_change_item_no_quota_check_quota_true(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
op = order.positions.first()
|
||||
payload = {
|
||||
'item': item2.pk,
|
||||
}
|
||||
assert op.item != item2
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/orderpositions/{}/?check_quotas=true'.format(
|
||||
organizer.slug, event.slug, op.pk
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_update_change_item_empty_quota_check_quota_true(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
q = event.quotas.create(name="No Quota", size=0)
|
||||
q.items.add(item2)
|
||||
q.save()
|
||||
op = order.positions.first()
|
||||
payload = {
|
||||
'item': item2.pk,
|
||||
}
|
||||
assert op.item != item2
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/orderpositions/{}/?check_quotas=true'.format(
|
||||
organizer.slug, event.slug, op.pk
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_update_change_item_variation(token_client, organizer, event, order, quota):
|
||||
with scopes_disabled():
|
||||
@@ -1318,6 +1418,49 @@ def test_position_update_change_subevent_quota_empty(token_client, organizer, ev
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_update_change_subevent_quota_empty_check_quota_false(token_client, organizer, event, order, quota, item, subevent):
|
||||
with scopes_disabled():
|
||||
se2 = event.subevents.create(name="Foobar", date_from=datetime.datetime(2017, 12, 27, 10, 0, 0, tzinfo=datetime.timezone.utc))
|
||||
q2 = se2.quotas.create(name="foo", size=0, event=event)
|
||||
q2.items.add(item)
|
||||
op = order.positions.first()
|
||||
op.subevent = subevent
|
||||
op.save()
|
||||
payload = {
|
||||
'subevent': se2.pk,
|
||||
}
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/orderpositions/{}/?check_quotas=false'.format(
|
||||
organizer.slug, event.slug, op.pk
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
op.refresh_from_db()
|
||||
assert op.subevent == se2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_update_change_subevent_quota_empty_check_quota_true(token_client, organizer, event, order, quota, item, subevent):
|
||||
with scopes_disabled():
|
||||
se2 = event.subevents.create(name="Foobar", date_from=datetime.datetime(2017, 12, 27, 10, 0, 0, tzinfo=datetime.timezone.utc))
|
||||
q2 = se2.quotas.create(name="foo", size=0, event=event)
|
||||
q2.items.add(item)
|
||||
op = order.positions.first()
|
||||
op.subevent = subevent
|
||||
op.save()
|
||||
payload = {
|
||||
'subevent': se2.pk,
|
||||
}
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/orderpositions/{}/?check_quotas=true'.format(
|
||||
organizer.slug, event.slug, op.pk
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_update_change_seat(token_client, organizer, event, order, quota, item, seat):
|
||||
with scopes_disabled():
|
||||
@@ -1600,6 +1743,85 @@ def test_position_add_quota_empty(token_client, organizer, event, order, quota,
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_add_no_quota(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
assert order.positions.count() == 1
|
||||
payload = {
|
||||
'order': order.code,
|
||||
'item': item2.pk,
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orderpositions/'.format(
|
||||
organizer.slug, event.slug,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_add_no_quota_check_quota_false(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
assert order.positions.count() == 1
|
||||
payload = {
|
||||
'order': order.code,
|
||||
'item': item2.pk,
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orderpositions/?check_quotas=false'.format(
|
||||
organizer.slug, event.slug,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_add_quota_empty_check_quota_false(token_client, organizer, event, order, quota, item):
|
||||
with scopes_disabled():
|
||||
assert order.positions.count() == 1
|
||||
quota.size = 1
|
||||
quota.save()
|
||||
payload = {
|
||||
'order': order.code,
|
||||
'item': item.pk,
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orderpositions/?check_quotas=false'.format(
|
||||
organizer.slug, event.slug,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
assert order.positions.count() == 2
|
||||
op = order.positions.last()
|
||||
assert op.item == item
|
||||
assert op.price == item.default_price
|
||||
assert op.positionid == 3
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_add_quota_empty_check_quota_true(token_client, organizer, event, order, quota, item):
|
||||
with scopes_disabled():
|
||||
assert order.positions.count() == 1
|
||||
quota.size = 1
|
||||
quota.save()
|
||||
payload = {
|
||||
'order': order.code,
|
||||
'item': item.pk,
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orderpositions/?check_quotas=true'.format(
|
||||
organizer.slug, event.slug,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_add_seat(token_client, organizer, event, order, quota, item, seat):
|
||||
with scopes_disabled():
|
||||
@@ -1803,6 +2025,283 @@ def test_order_change_patch(token_client, organizer, event, order, quota):
|
||||
assert order.total == Decimal('109.44')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_change_patch_no_quota(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
p = order.positions.first()
|
||||
payload = {
|
||||
'patch_positions': [
|
||||
{
|
||||
'position': p.pk,
|
||||
'body': {
|
||||
'item': item2.pk,
|
||||
'price': '99.44',
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/{}/change/'.format(
|
||||
organizer.slug, event.slug, order.code,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_change_patch_no_quota_check_quota_false(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
p = order.positions.first()
|
||||
payload = {
|
||||
'patch_positions': [
|
||||
{
|
||||
'position': p.pk,
|
||||
'body': {
|
||||
'item': item2.pk,
|
||||
'price': '99.44',
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/{}/change/?check_quotas=false'.format(
|
||||
organizer.slug, event.slug, order.code,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_change_patch_quota_empty(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
q = event.quotas.create(name="No Quota", size=0)
|
||||
q.items.add(item2)
|
||||
q.save()
|
||||
p = order.positions.first()
|
||||
payload = {
|
||||
'patch_positions': [
|
||||
{
|
||||
'position': p.pk,
|
||||
'body': {
|
||||
'item': item2.pk,
|
||||
'price': '99.44',
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/{}/change/'.format(
|
||||
organizer.slug, event.slug, order.code,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_change_patch_quota_empty_check_quota_false(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
q = event.quotas.create(name="No Quota", size=0)
|
||||
q.items.add(item2)
|
||||
q.save()
|
||||
p = order.positions.first()
|
||||
payload = {
|
||||
'patch_positions': [
|
||||
{
|
||||
'position': p.pk,
|
||||
'body': {
|
||||
'item': item2.pk,
|
||||
'price': '99.44',
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/{}/change/?check_quotas=false'.format(
|
||||
organizer.slug, event.slug, order.code,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
with scopes_disabled():
|
||||
p.refresh_from_db()
|
||||
assert p.price == Decimal('99.44')
|
||||
assert p.item == item2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_change_patch_quota_empty_check_quota_true(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
q = event.quotas.create(name="No Quota", size=0)
|
||||
q.items.add(item2)
|
||||
q.save()
|
||||
p = order.positions.first()
|
||||
payload = {
|
||||
'patch_positions': [
|
||||
{
|
||||
'position': p.pk,
|
||||
'body': {
|
||||
'item': item2.pk,
|
||||
'price': '99.44',
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/{}/change/?check_quotas=true'.format(
|
||||
organizer.slug, event.slug, order.code,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_change_create_position(token_client, organizer, event, order, quota, item):
|
||||
with scopes_disabled():
|
||||
assert order.positions.count() == 1
|
||||
payload = {
|
||||
'create_positions': [
|
||||
{
|
||||
'item': item.pk,
|
||||
},
|
||||
]
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/{}/change/'.format(
|
||||
organizer.slug, event.slug, order.code,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
with scopes_disabled():
|
||||
assert order.positions.count() == 2
|
||||
op = order.positions.last()
|
||||
assert op.item == item
|
||||
assert op.price == item.default_price
|
||||
assert op.positionid == 3
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_change_create_position_no_quota(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
payload = {
|
||||
'create_positions': [
|
||||
{
|
||||
'item': item2.pk,
|
||||
},
|
||||
]
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/{}/change/'.format(
|
||||
organizer.slug, event.slug, order.code,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_change_create_position_no_quota_check_quota_false(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
payload = {
|
||||
'create_positions': [
|
||||
{
|
||||
'item': item2.pk,
|
||||
},
|
||||
]
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/{}/change/?check_quotas=false'.format(
|
||||
organizer.slug, event.slug, order.code,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_change_create_position_quota_empty(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
q = event.quotas.create(name="No Quota", size=0)
|
||||
q.items.add(item2)
|
||||
q.save()
|
||||
payload = {
|
||||
'create_positions': [
|
||||
{
|
||||
'item': item2.pk,
|
||||
},
|
||||
]
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/{}/change/'.format(
|
||||
organizer.slug, event.slug, order.code,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_change_create_position_quota_empty_check_quota_false(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
assert order.positions.count() == 1
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
q = event.quotas.create(name="No Quota", size=0)
|
||||
q.items.add(item2)
|
||||
q.save()
|
||||
payload = {
|
||||
'create_positions': [
|
||||
{
|
||||
'item': item2.pk,
|
||||
},
|
||||
]
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/{}/change/?check_quotas=false'.format(
|
||||
organizer.slug, event.slug, order.code,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
with scopes_disabled():
|
||||
assert order.positions.count() == 2
|
||||
op = order.positions.last()
|
||||
assert op.item == item2
|
||||
assert op.price == item2.default_price
|
||||
assert op.positionid == 3
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_change_create_position_quota_empty_check_quota_true(token_client, organizer, event, order):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
q = event.quotas.create(name="No Quota", size=0)
|
||||
q.items.add(item2)
|
||||
q.save()
|
||||
payload = {
|
||||
'create_positions': [
|
||||
{
|
||||
'item': item2.pk,
|
||||
},
|
||||
]
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/{}/change/?check_quotas=true'.format(
|
||||
organizer.slug, event.slug, order.code,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'quota' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_change_cancel_and_create(token_client, organizer, event, order, quota, item):
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -91,6 +91,7 @@ def test_voucher_list(token_client, organizer, event, voucher, item, quota, sube
|
||||
res = dict(TEST_VOUCHER_RES)
|
||||
res['item'] = item.pk
|
||||
res['id'] = voucher.pk
|
||||
res['created'] = voucher.created.isoformat().replace('+00:00', 'Z')
|
||||
res['code'] = voucher.code
|
||||
q2 = copy.copy(quota)
|
||||
q2.pk = None
|
||||
@@ -264,6 +265,7 @@ def test_voucher_detail(token_client, organizer, event, voucher, item):
|
||||
res['item'] = item.pk
|
||||
res['id'] = voucher.pk
|
||||
res['code'] = voucher.code
|
||||
res['created'] = voucher.created.isoformat().replace('+00:00', 'Z')
|
||||
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/vouchers/{}/'.format(organizer.slug, event.slug,
|
||||
voucher.pk))
|
||||
|
||||
@@ -1337,6 +1337,57 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
|
||||
self.assertRedirects(response, '/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
|
||||
def test_invoice_address_discarded_for_free(self):
|
||||
self.event.settings.invoice_address_asked = True
|
||||
self.event.settings.invoice_address_required = True
|
||||
self.event.settings.invoice_address_not_asked_free = True
|
||||
self.event.settings.set('name_scheme', 'title_given_middle_family')
|
||||
|
||||
with scopes_disabled():
|
||||
cp = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.content.decode(), "lxml")
|
||||
self.assertEqual(len(doc.select('input[name="city"]')), 1)
|
||||
|
||||
# Corrected request
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'is_business': 'business',
|
||||
'company': 'Foo',
|
||||
'name_parts_0': 'Mr',
|
||||
'name_parts_1': 'John',
|
||||
'name_parts_2': '',
|
||||
'name_parts_3': 'Kennedy',
|
||||
'street': 'Baz',
|
||||
'zipcode': '12345',
|
||||
'city': 'Here',
|
||||
'country': 'DE',
|
||||
'vat_id': 'DE123456',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
with scopes_disabled():
|
||||
assert InvoiceAddress.objects.exists()
|
||||
|
||||
cp.price = Decimal("0.00")
|
||||
cp.save()
|
||||
|
||||
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.content.decode(), "lxml")
|
||||
self.assertEqual(len(doc.select('input[name="city"]')), 0)
|
||||
|
||||
# Corrected request
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
with scopes_disabled():
|
||||
assert not InvoiceAddress.objects.exists()
|
||||
|
||||
def test_invoice_address_optional(self):
|
||||
self.event.settings.invoice_address_asked = True
|
||||
self.event.settings.invoice_address_required = False
|
||||
|
||||
Reference in New Issue
Block a user