mirror of
https://github.com/pretix/pretix.git
synced 2026-03-16 14:42:28 +00:00
Compare commits
42 Commits
peppol-be-
...
file-dispo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d28fcf2319 | ||
|
|
d1df2e5213 | ||
|
|
5b93687611 | ||
|
|
26ccfee67b | ||
|
|
bc1eff4e56 | ||
|
|
c53b10e04b | ||
|
|
12295fde84 | ||
|
|
094193efc8 | ||
|
|
3b2b00bf67 | ||
|
|
35822e7f25 | ||
|
|
e68189a2ea | ||
|
|
e4dae627d6 | ||
|
|
3550a23e33 | ||
|
|
2d988406e3 | ||
|
|
ed1966bc96 | ||
|
|
fad5284f25 | ||
|
|
f57530d3ff | ||
|
|
1427edf5ab | ||
|
|
4898475d56 | ||
|
|
cdacc84553 | ||
|
|
ef483d5229 | ||
|
|
ad6f5a7b54 | ||
|
|
ecc49d453d | ||
|
|
c45070b190 | ||
|
|
aea2a1ca10 | ||
|
|
d791b9e108 | ||
|
|
2c9802d1cb | ||
|
|
c39f1bfcc2 | ||
|
|
894128deab | ||
|
|
3352ee2bbe | ||
|
|
af28785fb9 | ||
|
|
54e4957e89 | ||
|
|
f3597f1a44 | ||
|
|
2e01887e79 | ||
|
|
5a7e7fbde3 | ||
|
|
7b296107c5 | ||
|
|
4f449ce6b4 | ||
|
|
e6ea8fb5bf | ||
|
|
547910beec | ||
|
|
eef1560ede | ||
|
|
3d68bbb619 | ||
|
|
dc4556d428 |
@@ -117,6 +117,8 @@ cancellation_date datetime Time of order c
|
||||
reliable for orders that have been cancelled,
|
||||
reactivated and cancelled again.
|
||||
plugin_data object Additional data added by plugins.
|
||||
use_gift_cards list of strings List of unique gift card secrets that are used to pay
|
||||
for this order.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -156,6 +158,10 @@ plugin_data object Additional data
|
||||
|
||||
The ``tax_rounding_mode`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2026.03
|
||||
|
||||
The ``use_gift_cards`` attribute has been added.
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
Order position resource
|
||||
@@ -987,8 +993,6 @@ Creating orders
|
||||
|
||||
* does not support file upload questions
|
||||
|
||||
* does not support redeeming gift cards
|
||||
|
||||
* does not support or validate memberships
|
||||
|
||||
|
||||
@@ -1095,6 +1099,14 @@ Creating orders
|
||||
whether these emails are enabled for certain sales channels. If set to ``null``, behavior will be controlled by pretix'
|
||||
settings based on the sales channels (added in pretix 4.7). Defaults to ``false``.
|
||||
Used to be ``send_mail`` before pretix 3.14.
|
||||
* ``use_gift_cards`` (optional) The provided gift cards will be used to pay for this order. They will be debited and
|
||||
all the necessary payment records for these transactions will be created. The gift cards will be used in sequence to
|
||||
pay for the order. Processing of the gift cards stops as soon as the order is payed for. All gift card transactions
|
||||
are listed under ``payments`` in the response.
|
||||
This option can only be used with orders that are in the pending state.
|
||||
The ``use_gift_cards`` attribute can not be combined with ``payment_info`` and ``payment_provider`` fields. If the
|
||||
order isn't completely paid after its creation with ``use_gift_cards``, then a subsequent request to the payment
|
||||
endpoint is needed.
|
||||
|
||||
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
|
||||
to incrementing integers starting with ``1``. Then, you can reference one of these
|
||||
|
||||
@@ -73,7 +73,7 @@ dependencies = [
|
||||
"packaging",
|
||||
"paypalrestsdk==1.13.*",
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.11.*",
|
||||
"PyJWT==2.12.*",
|
||||
"phonenumberslite==9.0.*",
|
||||
"Pillow==12.1.*",
|
||||
"pretix-plugin-build",
|
||||
|
||||
@@ -53,7 +53,7 @@ from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.invoicing.transmission import get_transmission_types
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Customer, Device, Invoice, InvoiceAddress,
|
||||
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
|
||||
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
||||
QuestionAnswer, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
|
||||
Voucher,
|
||||
@@ -62,6 +62,7 @@ from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
PrintLog, RevokedTicketSecret, Transaction,
|
||||
)
|
||||
from pretix.base.payment import GiftCardPayment, PaymentException
|
||||
from pretix.base.pdf import get_images, get_variables
|
||||
from pretix.base.services.cart import error_messages
|
||||
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
|
||||
@@ -1200,6 +1201,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
tax_rounding_mode = serializers.ChoiceField(choices=ROUNDING_MODES, allow_null=True, required=False,)
|
||||
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
|
||||
use_gift_cards = serializers.ListField(child=serializers.CharField(required=False), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -1215,7 +1217,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode')
|
||||
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode', 'use_gift_cards')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
if pp is None:
|
||||
@@ -1310,6 +1312,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
payment_date = validated_data.pop('payment_date', now())
|
||||
force = validated_data.pop('force', False)
|
||||
simulate = validated_data.pop('simulate', False)
|
||||
gift_card_secrets = validated_data.pop('use_gift_cards') if 'use_gift_cards' in validated_data else []
|
||||
|
||||
if (payment_provider is not None or payment_info != '{}') and len(gift_card_secrets) > 0:
|
||||
raise ValidationError({"use_gift_cards": ['The attribute use_gift_cards is not compatible with payment_provider or payment_info']})
|
||||
if validated_data.get('status') != Order.STATUS_PENDING and len(gift_card_secrets) > 0:
|
||||
raise ValidationError({"use_gift_cards": ['The attribute use_gift_cards is only supported for orders that are created as pending']})
|
||||
if len(set(gift_card_secrets)) != len(gift_card_secrets):
|
||||
raise ValidationError({"use_gift_cards": ['Multiple copies of the same gift card secret are not allowed']})
|
||||
|
||||
if not validated_data.get("sales_channel"):
|
||||
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
|
||||
@@ -1794,6 +1804,45 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if order.total != Decimal('0.00') and order.event.currency == "XXX":
|
||||
raise ValidationError('Paid products not supported without a valid currency.')
|
||||
|
||||
for gift_card_secret in gift_card_secrets:
|
||||
try:
|
||||
if order.status != Order.STATUS_PAID:
|
||||
gift_card_payment_provider = GiftCardPayment(event=order.event)
|
||||
|
||||
gc = order.event.organizer.accepted_gift_cards.get(
|
||||
secret=gift_card_secret
|
||||
)
|
||||
|
||||
payment = order.payments.create(
|
||||
amount=min(order.pending_sum, gc.value),
|
||||
provider=gift_card_payment_provider.identifier,
|
||||
info_data={
|
||||
'gift_card': gc.pk,
|
||||
'gift_card_secret': gc.secret,
|
||||
'retry': True
|
||||
},
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED
|
||||
)
|
||||
gift_card_payment_provider.execute_payment(request=None, payment=payment, is_early_special_case=True)
|
||||
|
||||
if order.pending_sum <= Decimal('0.00'):
|
||||
order.status = Order.STATUS_PAID
|
||||
|
||||
except PaymentException:
|
||||
pass
|
||||
|
||||
except GiftCard.DoesNotExist as e:
|
||||
payment = order.payments.create(
|
||||
amount=order.pending_sum,
|
||||
provider=GiftCardPayment.identifier,
|
||||
info_data={
|
||||
'gift_card_secret': gift_card_secret,
|
||||
},
|
||||
state=OrderPayment.PAYMENT_STATE_CREATED
|
||||
)
|
||||
payment.fail(info={**payment.info_data, 'error': str(e)},
|
||||
send_mail=False)
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
|
||||
@@ -381,12 +381,15 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), order.code,
|
||||
provider.identifier, ct.extension
|
||||
return FileResponse(
|
||||
ct.file.file,
|
||||
filename='{}-{}-{}{}'.format(
|
||||
self.request.event.slug.upper(), order.code,
|
||||
provider.identifier, ct.extension
|
||||
),
|
||||
as_attachment=True,
|
||||
content_type=ct.type
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def mark_paid(self, request, **kwargs):
|
||||
@@ -1301,14 +1304,17 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
|
||||
raise NotFound()
|
||||
|
||||
ftype, ignored = mimetypes.guess_type(answer.file.name)
|
||||
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
|
||||
self.request.event.slug.upper(),
|
||||
pos.order.code,
|
||||
pos.positionid,
|
||||
os.path.basename(answer.file.name).split('.', 1)[1]
|
||||
return FileResponse(
|
||||
answer.file,
|
||||
filename='{}-{}-{}-{}"'.format(
|
||||
self.request.event.slug.upper(),
|
||||
pos.order.code,
|
||||
pos.positionid,
|
||||
os.path.basename(answer.file.name).split('.', 1)[1]
|
||||
),
|
||||
as_attachment=True,
|
||||
content_type=ftype or 'application/binary'
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
|
||||
def printlog(self, request, **kwargs):
|
||||
@@ -1363,15 +1369,18 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
|
||||
if hasattr(image_file, 'seek'):
|
||||
image_file.seek(0)
|
||||
|
||||
resp = FileResponse(image_file, content_type=ftype or 'application/binary')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}.{}"'.format(
|
||||
self.request.event.slug.upper(),
|
||||
pos.order.code,
|
||||
pos.positionid,
|
||||
key,
|
||||
extension,
|
||||
return FileResponse(
|
||||
image_file,
|
||||
filename='{}-{}-{}-{}.{}'.format(
|
||||
self.request.event.slug.upper(),
|
||||
pos.order.code,
|
||||
pos.positionid,
|
||||
key,
|
||||
extension,
|
||||
),
|
||||
as_attachment=True,
|
||||
content_type=ftype or 'application/binary'
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
|
||||
def download(self, request, output, **kwargs):
|
||||
@@ -1397,12 +1406,15 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
|
||||
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), pos.order.code, pos.positionid,
|
||||
provider.identifier, ct.extension
|
||||
return FileResponse(
|
||||
ct.file.file,
|
||||
filename='{}-{}-{}-{}{}'.format(
|
||||
self.request.event.slug.upper(), pos.order.code, pos.positionid,
|
||||
provider.identifier, ct.extension
|
||||
),
|
||||
as_attachment=True,
|
||||
content_type=ct.type
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def regenerate_secrets(self, request, **kwargs):
|
||||
@@ -1979,9 +1991,12 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
if not invoice.file:
|
||||
raise RetryException()
|
||||
|
||||
resp = FileResponse(invoice.file.file, content_type='application/pdf')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
|
||||
return resp
|
||||
return FileResponse(
|
||||
invoice.file.file,
|
||||
filename='{}.pdf"'.format(invoice.number),
|
||||
as_attachment=True,
|
||||
content_type='application/pdf'
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def transmit(self, request, **kwargs):
|
||||
|
||||
@@ -315,8 +315,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for id, vn in payment_methods:
|
||||
headers.append(_('Paid by {method}').format(method=vn))
|
||||
|
||||
# get meta_data labels from first cached event
|
||||
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
|
||||
if self.event_object_cache:
|
||||
# get meta_data labels from first cached event if any
|
||||
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
|
||||
yield headers
|
||||
|
||||
full_fee_sum_cache = {
|
||||
@@ -503,8 +504,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(_('External customer ID'))
|
||||
headers.append(_('Payment providers'))
|
||||
|
||||
# get meta_data labels from first cached event
|
||||
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
|
||||
if self.event_object_cache:
|
||||
# get meta_data labels from first cached event if any
|
||||
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
|
||||
yield headers
|
||||
|
||||
yield self.ProgressSetTotal(total=qs.count())
|
||||
@@ -707,9 +709,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
_('Position order link')
|
||||
]
|
||||
|
||||
# get meta_data labels from first cached event
|
||||
meta_data_labels = next(iter(self.event_object_cache.values())).meta_data.keys()
|
||||
if has_subevents:
|
||||
# get meta_data labels from first cached event
|
||||
meta_data_labels = next(iter(self.event_object_cache.values())).meta_data.keys()
|
||||
headers += meta_data_labels
|
||||
yield headers
|
||||
|
||||
|
||||
@@ -33,8 +33,7 @@ from pretix.base.invoicing.transmission import (
|
||||
transmission_types,
|
||||
)
|
||||
from pretix.base.models import Invoice, InvoiceAddress
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.base.services.mail import mail
|
||||
|
||||
|
||||
@transmission_types.new()
|
||||
@@ -134,9 +133,7 @@ class EmailTransmissionProvider(TransmissionProvider):
|
||||
subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString)
|
||||
|
||||
# Do not set to completed because that is done by the email sending task
|
||||
subject = format_map(subject, context)
|
||||
email_content = render_mail(template, context)
|
||||
mail(
|
||||
outgoing_mail = mail(
|
||||
[recipient],
|
||||
subject,
|
||||
template,
|
||||
@@ -151,19 +148,10 @@ class EmailTransmissionProvider(TransmissionProvider):
|
||||
plain_text_only=True,
|
||||
no_order_links=True,
|
||||
)
|
||||
invoice.order.log_action(
|
||||
'pretix.event.order.email.invoice',
|
||||
user=None,
|
||||
auth=None,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'position': None,
|
||||
'recipient': recipient,
|
||||
'invoices': [invoice.pk],
|
||||
'attach_tickets': False,
|
||||
'attach_ical': False,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': [],
|
||||
}
|
||||
)
|
||||
if outgoing_mail:
|
||||
invoice.order.log_action(
|
||||
'pretix.event.order.email.invoice',
|
||||
user=None,
|
||||
auth=None,
|
||||
data=outgoing_mail.log_data()
|
||||
)
|
||||
|
||||
@@ -220,3 +220,20 @@ class OutgoingMail(models.Model):
|
||||
error_log_action_type = 'pretix.email.error'
|
||||
log_target = None
|
||||
return log_target, error_log_action_type
|
||||
|
||||
def log_data(self):
|
||||
return {
|
||||
"subject": self.subject,
|
||||
"message": self.body_plain,
|
||||
"to": self.to,
|
||||
"cc": self.cc,
|
||||
"bcc": self.bcc,
|
||||
|
||||
"invoices": [i.pk for i in self.should_attach_invoices.all()],
|
||||
"attach_tickets": self.should_attach_tickets,
|
||||
"attach_ical": self.should_attach_ical,
|
||||
"attach_other_files": self.should_attach_other_files,
|
||||
"attach_cached_files": [cf.filename for cf in self.should_attach_cached_files.all()],
|
||||
|
||||
"position": self.orderposition.positionid if self.orderposition else None,
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@ from pretix.base.timemachine import time_machine_now
|
||||
|
||||
from ...helpers import OF_SELF
|
||||
from ...helpers.countries import CachedCountries, FastCountryField
|
||||
from ...helpers.format import FormattedString, format_map
|
||||
from ...helpers.names import build_name
|
||||
from ...testutils.middleware import debugflags_var
|
||||
from ._transactions import (
|
||||
@@ -1167,7 +1166,7 @@ class Order(LockModel, LoggedModel):
|
||||
only be attached for this position and child positions, the link will only point to the
|
||||
position and the attendee email will be used if available.
|
||||
"""
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
from pretix.base.services.mail import mail
|
||||
|
||||
if not self.email and not (position and position.attendee_email):
|
||||
return
|
||||
@@ -1177,32 +1176,20 @@ class Order(LockModel, LoggedModel):
|
||||
if position and position.attendee_email:
|
||||
recipient = position.attendee_email
|
||||
|
||||
email_content = render_mail(template, context)
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
outgoing_mail = mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.locale, self, headers=headers, sender=sender,
|
||||
invoices=invoices, attach_tickets=attach_tickets,
|
||||
position=position, auto_email=auto_email, attach_ical=attach_ical,
|
||||
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
|
||||
)
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'position': position.positionid if position else None,
|
||||
'recipient': recipient,
|
||||
'invoices': [i.pk for i in invoices] if invoices else [],
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
|
||||
}
|
||||
)
|
||||
if outgoing_mail:
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
|
||||
def resend_link(self, user=None, auth=None):
|
||||
with language(self.locale, self.event.settings.region):
|
||||
@@ -2900,17 +2887,14 @@ class OrderPosition(AbstractPosition):
|
||||
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
|
||||
:param attach_ical: Attach relevant ICS files
|
||||
"""
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
from pretix.base.services.mail import mail
|
||||
|
||||
if not self.attendee_email:
|
||||
return
|
||||
|
||||
with language(self.order.locale, self.order.event.settings.region):
|
||||
recipient = self.attendee_email
|
||||
email_content = render_mail(template, context)
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
outgoing_mail = mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
|
||||
position=self,
|
||||
@@ -2919,21 +2903,13 @@ class OrderPosition(AbstractPosition):
|
||||
attach_ical=attach_ical,
|
||||
attach_other_files=attach_other_files,
|
||||
)
|
||||
self.order.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'recipient': recipient,
|
||||
'invoices': [i.pk for i in invoices] if invoices else [],
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [],
|
||||
}
|
||||
)
|
||||
if outgoing_mail:
|
||||
self.order.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
|
||||
def resend_link(self, user=None, auth=None):
|
||||
|
||||
|
||||
@@ -34,10 +34,9 @@ from phonenumber_field.modelfields import PhoneNumberField
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import User, Voucher
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.helpers import OF_SELF
|
||||
|
||||
from ...helpers.format import format_map
|
||||
from ...helpers.names import build_name
|
||||
from .base import LoggedModel
|
||||
from .event import Event, SubEvent
|
||||
@@ -273,9 +272,7 @@ class WaitingListEntry(LoggedModel):
|
||||
with language(self.locale, self.event.settings.region):
|
||||
recipient = self.email
|
||||
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
outgoing_mail = mail(
|
||||
recipient, subject, template, context,
|
||||
self.event,
|
||||
self.locale,
|
||||
@@ -285,18 +282,13 @@ class WaitingListEntry(LoggedModel):
|
||||
attach_other_files=attach_other_files,
|
||||
attach_cached_files=attach_cached_files,
|
||||
)
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'recipient': recipient,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
|
||||
}
|
||||
)
|
||||
if outgoing_mail:
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def clean_itemvar(event, item, variation):
|
||||
|
||||
@@ -1295,6 +1295,7 @@ class ManualPayment(BasePaymentProvider):
|
||||
|
||||
def format_map(self, order, payment):
|
||||
return {
|
||||
# Possible placeholder injection, we should make sure to never include user-controlled variables here
|
||||
'order': order.code,
|
||||
'amount': payment.amount,
|
||||
'currency': self.event.currency,
|
||||
@@ -1525,16 +1526,26 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
def payment_control_render(self, request, payment) -> str:
|
||||
from .models import GiftCard
|
||||
|
||||
if 'gift_card' in payment.info_data:
|
||||
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
|
||||
if any(key in payment.info_data for key in ('gift_card', 'error')):
|
||||
template = get_template('pretixcontrol/giftcards/payment.html')
|
||||
|
||||
ctx = {
|
||||
'request': request,
|
||||
'event': self.event,
|
||||
'gc': gc,
|
||||
**({'error': payment.info_data[
|
||||
'error']} if 'error' in payment.info_data else {}),
|
||||
**({'gift_card_secret': payment.info_data[
|
||||
'gift_card_secret']} if 'gift_card_secret' in payment.info_data else {})
|
||||
}
|
||||
return template.render(ctx)
|
||||
|
||||
try:
|
||||
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
|
||||
ctx = {
|
||||
'gc': gc,
|
||||
}
|
||||
except GiftCard.DoesNotExist:
|
||||
pass
|
||||
finally:
|
||||
return template.render(ctx)
|
||||
|
||||
def payment_control_render_short(self, payment: OrderPayment) -> str:
|
||||
d = payment.info_data
|
||||
@@ -1549,12 +1560,16 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
try:
|
||||
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
|
||||
except GiftCard.DoesNotExist:
|
||||
return {}
|
||||
return {
|
||||
**({'error': payment.info_data[
|
||||
'error']} if 'error' in payment.info_data else {})
|
||||
}
|
||||
return {
|
||||
'gift_card': {
|
||||
'id': gc.pk,
|
||||
'secret': gc.secret,
|
||||
'organizer': gc.issuer.slug
|
||||
'organizer': gc.issuer.slug,
|
||||
** ({'error': payment.info_data['error']} if 'error' in payment.info_data else {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1626,6 +1641,8 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
raise PaymentException(_("This gift card does not support this currency."))
|
||||
if not gc.accepted_by(self.event.organizer):
|
||||
raise PaymentException(_("This gift card is not accepted by this event organizer."))
|
||||
if gc.value <= Decimal("0.00"):
|
||||
raise PaymentException(_("All credit on this gift card has been used."))
|
||||
if payment.amount > gc.value:
|
||||
raise PaymentException(_("This gift card was used in the meantime. Please try again."))
|
||||
if gc.testmode and not payment.order.testmode:
|
||||
@@ -1655,7 +1672,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
}
|
||||
)
|
||||
except PaymentException as e:
|
||||
payment.fail(info={'error': str(e)})
|
||||
payment.fail(info={**payment.info_data, 'error': str(e)}, send_mail=not is_early_special_case)
|
||||
raise e
|
||||
|
||||
def payment_is_valid_session(self, request: HttpRequest) -> bool:
|
||||
|
||||
@@ -45,7 +45,6 @@ from pretix.base.services.tax import split_fee_for_taxes
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.format import format_map
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,7 +54,7 @@ def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: Lazy
|
||||
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
|
||||
mail(
|
||||
wle.email,
|
||||
format_map(subject, email_context),
|
||||
str(subject),
|
||||
message,
|
||||
email_context,
|
||||
wle.event,
|
||||
@@ -73,9 +72,8 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
||||
|
||||
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
|
||||
order=order, position_or_address=ia, event=order.event)
|
||||
real_subject = format_map(subject, email_context)
|
||||
order.send_mail(
|
||||
real_subject, message, email_context,
|
||||
subject, message, email_context,
|
||||
'pretix.event.order.email.event_canceled',
|
||||
user,
|
||||
)
|
||||
@@ -85,14 +83,13 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
||||
continue
|
||||
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||
real_subject = format_map(subject, email_context)
|
||||
email_context = get_email_context(event_or_subevent=p.subevent or order.event,
|
||||
event=order.event,
|
||||
refund_amount=refund_amount,
|
||||
position_or_address=p,
|
||||
order=order, position=p)
|
||||
order.send_mail(
|
||||
real_subject, message, email_context,
|
||||
subject, message, email_context,
|
||||
'pretix.event.order.email.event_canceled',
|
||||
position=p,
|
||||
user=user
|
||||
|
||||
@@ -334,7 +334,8 @@ def _check_position_constraints(
|
||||
raise CartPositionError(error_messages['voucher_invalid_subevent'])
|
||||
|
||||
# Voucher expired
|
||||
if voucher and voucher.valid_until and voucher.valid_until < time_machine_now_dt:
|
||||
# (checked using real_now_dt as vouchers influence quota calculations)
|
||||
if voucher and voucher.valid_until and voucher.valid_until < real_now_dt:
|
||||
raise CartPositionError(error_messages['voucher_expired'])
|
||||
|
||||
# Subevent has been disabled
|
||||
|
||||
@@ -149,13 +149,13 @@ def prefix_subject(settings_holder, subject, highlight=False):
|
||||
return subject
|
||||
|
||||
|
||||
def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString],
|
||||
def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString], template: Union[str, LazyI18nString],
|
||||
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
|
||||
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
|
||||
customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None,
|
||||
attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None,
|
||||
plain_text_only=False, no_order_links=False, cc: Sequence[str]=None, bcc: Sequence[str]=None,
|
||||
sensitive: bool=False):
|
||||
sensitive: bool=False) -> Optional[OutgoingMail]:
|
||||
"""
|
||||
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
|
||||
|
||||
@@ -335,14 +335,26 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
should_attach_other_files=attach_other_files or [],
|
||||
sensitive=sensitive,
|
||||
)
|
||||
m._prefetched_objects_cache = {}
|
||||
if invoices and not position:
|
||||
m.should_attach_invoices.add(*invoices)
|
||||
# Hack: For logging, we'll later make a `should_attach_invoices.all()` call. We can prevent a useless
|
||||
# DB query by filling the cache
|
||||
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = invoices
|
||||
else:
|
||||
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = Invoice.objects.none()
|
||||
if attach_cached_files:
|
||||
cf_list = []
|
||||
for cf in attach_cached_files:
|
||||
if not isinstance(cf, CachedFile):
|
||||
m.should_attach_cached_files.add(CachedFile.objects.get(pk=cf))
|
||||
else:
|
||||
m.should_attach_cached_files.add(cf)
|
||||
cf = CachedFile.objects.get(pk=cf)
|
||||
m.should_attach_cached_files.add(cf)
|
||||
cf_list.append(cf)
|
||||
# Hack: For logging, we'll later make a `should_attach_cached_files.all()` call. We can prevent a useless
|
||||
# DB query by filling the cache
|
||||
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = cf_list
|
||||
else:
|
||||
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = CachedFile.objects.none()
|
||||
|
||||
send_task = mail_send_task.si(
|
||||
outgoing_mail=m.id
|
||||
@@ -364,6 +376,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
lambda: chain(*task_chain).apply_async()
|
||||
)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
class CustomEmail(EmailMultiAlternatives):
|
||||
def _create_mime_attachment(self, content, mimetype):
|
||||
|
||||
@@ -1799,8 +1799,6 @@ class OrderChangeManager:
|
||||
tax_rule = tax_rules.get(pos.pk, pos.tax_rule)
|
||||
if not tax_rule:
|
||||
continue
|
||||
if not pos.price:
|
||||
continue
|
||||
|
||||
try:
|
||||
new_rate = tax_rule.tax_rate_for(ia)
|
||||
@@ -1817,7 +1815,9 @@ class OrderChangeManager:
|
||||
override_tax_rate=new_rate, override_tax_code=new_code)
|
||||
self._totaldiff_guesstimate += new_tax.gross - pos.price
|
||||
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
|
||||
self._invoice_dirty = True
|
||||
if pos.price:
|
||||
# We do not consider the invoice dirty if only 0€-valued taxes are changed
|
||||
self._invoice_dirty = True
|
||||
|
||||
def cancel_fee(self, fee: OrderFee):
|
||||
self._totaldiff_guesstimate -= fee.value
|
||||
|
||||
@@ -39,7 +39,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
||||
with language(event.settings.locale):
|
||||
email_context = get_email_context(event=event, name=r.get('name') or '',
|
||||
voucher_list=[v.code for v in voucher_list])
|
||||
mail(
|
||||
outgoing_mail = mail(
|
||||
r['email'],
|
||||
subject,
|
||||
LazyI18nString(message),
|
||||
@@ -60,8 +60,8 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
||||
data={
|
||||
'recipient': r['email'],
|
||||
'name': r.get('name'),
|
||||
'subject': subject,
|
||||
'message': message,
|
||||
'subject': outgoing_mail.subject,
|
||||
'message': outgoing_mail.body_plain,
|
||||
},
|
||||
save=False
|
||||
))
|
||||
|
||||
@@ -363,7 +363,7 @@ class EmailAddressShredder(BaseDataShredder):
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
else:
|
||||
shred_log_fields(le, banlist=[
|
||||
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email'
|
||||
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email', 'bcc', 'cc',
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
|
||||
{% block custom_header %}{% endblock %}
|
||||
{% if css_theme %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
@@ -641,6 +641,7 @@ class TeamMembershipLogEntryType(LogEntryType):
|
||||
'pretix.team.member.added': _('{user} has been added to the team.'),
|
||||
'pretix.team.member.removed': _('{user} has been removed from the team.'),
|
||||
'pretix.team.invite.created': _('{user} has been invited to the team.'),
|
||||
'pretix.team.invite.deleted': _('Invite for {user} has been deleted.'),
|
||||
'pretix.team.invite.resent': _('Invite for {user} has been resent.'),
|
||||
})
|
||||
class CoreTeamMembershipLogEntryType(TeamMembershipLogEntryType):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Events" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Events" %}</h1>
|
||||
@@ -74,6 +75,7 @@
|
||||
<a href="?{% url_replace request 'ordering' 'organizer' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Sales channels" %}</th>
|
||||
<th>
|
||||
{% trans "Start date" %}
|
||||
<a href="?{% url_replace request 'ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a>
|
||||
@@ -108,6 +110,21 @@
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% if not hide_orga %}<td>{{ e.organizer }}</td>{% endif %}
|
||||
<td>
|
||||
{% for c in e.organizer.sales_channels.all %}
|
||||
{% if e.all_sales_channels or c in e.limit_sales_channels.all %}
|
||||
{% if "." in c.icon %}
|
||||
<img src="{% static c.icon %}" class="fa-like-image"
|
||||
data-toggle="tooltip" title="{{ c.label }}">
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{{ c.label }}"></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="fa fa-fw"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="event-date-col">
|
||||
{% if e.has_subevents %}
|
||||
<span class="fa fa-fw- fa-calendar"></span>
|
||||
|
||||
@@ -3,16 +3,26 @@
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Gift card code" %}</dt>
|
||||
<dd>
|
||||
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
|
||||
{{ gc.secret }}
|
||||
</a>
|
||||
{% if gc.issuer != request.organizer %}
|
||||
<span class="text-muted">
|
||||
<br>
|
||||
<span class="fa fa-group"></span> {{ gc.issuer }}
|
||||
</span>
|
||||
{% if gc %}
|
||||
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
|
||||
{{ gc.secret }}
|
||||
</a>
|
||||
{% if gc.issuer.slug != request.organizer %}
|
||||
<span class="text-muted">
|
||||
<br>
|
||||
<span class="fa fa-group"></span> {{ gc.issuer }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% elif gift_card_secret %}
|
||||
{{ gift_card_secret }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{% trans "Issuer" %}</dt>
|
||||
<dd>{{ gc.issuer }}</dd>
|
||||
{% if gc %}
|
||||
<dt>{% trans "Issuer" %}</dt>
|
||||
<dd>{{ gc.issuer }}</dd>
|
||||
{% endif %}
|
||||
{% if error %}
|
||||
<dt>{% trans "Error" %}</dt>
|
||||
<dd>{{ error }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
{% if log.display %}
|
||||
<br/><span class="fa fa-fw fa-comment-o"></span> {{ log.display }}
|
||||
{% endif %}
|
||||
{% if log.parsed_data.recipient %}
|
||||
{% if log.parsed_data.to %}
|
||||
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.to|join:", " }}
|
||||
{% elif log.parsed_data.recipient %} {# legacy #}
|
||||
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.recipient }}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% blocktrans with name=request.organizer.name %}Organizer: {{ name }}{% endblocktrans %}
|
||||
@@ -62,6 +63,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Event name" %}</th>
|
||||
<th>{% trans "Sales channels" %}</th>
|
||||
<th>
|
||||
{% trans "Start date" %}
|
||||
/
|
||||
@@ -77,10 +79,30 @@
|
||||
<td>
|
||||
<strong><a
|
||||
href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
|
||||
<br><small>{{ e.slug }}</small>
|
||||
{% for k, v in e.meta_data.items %}
|
||||
{% if v %}
|
||||
<small class="text-muted">· {{ k }}: {{ v }}</small>
|
||||
<br>
|
||||
<small>
|
||||
{{ e.slug }}
|
||||
</small>
|
||||
<small class="text-muted">
|
||||
{% for k, v in e.meta_data.items %}
|
||||
{% if v %}
|
||||
· {{ k }}: {{ v }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{% for c in sales_channels %}
|
||||
{% if e.all_sales_channels or c in e.limit_sales_channels.all %}
|
||||
{% if "." in c.icon %}
|
||||
<img src="{% static c.icon %}" class="fa-like-image"
|
||||
data-toggle="tooltip" title="{{ c.label }}">
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{{ c.label }}"></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="fa fa-fw"></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
|
||||
@@ -264,12 +264,17 @@
|
||||
The paper size will match the PDF.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
<p class="text-center">
|
||||
<span class="btn btn-default fileinput-button background-button btn-block">
|
||||
<i class="fa fa-upload"></i>
|
||||
<span>{% trans "Upload PDF as background" %}</span>
|
||||
<input id="fileupload" type="file" name="background" accept="application/pdf">
|
||||
</span>
|
||||
<small class="text-muted">
|
||||
{% blocktrans trimmed with size=maxfilesize|filesizeformat %}
|
||||
max. {{ size }}, smaller is better
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</p>
|
||||
<p class="text-center">
|
||||
<a class="btn btn-link background-download-button" href="{{ pdf }}" target="_blank">
|
||||
|
||||
@@ -743,12 +743,7 @@ class InvoicePreview(EventPermissionRequiredMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
fname, ftype, fcontent = build_preview_invoice_pdf(request.event)
|
||||
resp = HttpResponse(fcontent, content_type=ftype)
|
||||
if settings.DEBUG:
|
||||
# attachment is more secure as we're dealing with user-generated stuff here, but inline is much more convenient during debugging
|
||||
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
|
||||
resp._csp_ignore = True
|
||||
else:
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(fname)
|
||||
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
|
||||
return resp
|
||||
|
||||
|
||||
|
||||
@@ -300,5 +300,4 @@ class SysReportView(AdministratorPermissionRequiredMixin, TemplateView):
|
||||
resp = HttpResponse(data)
|
||||
resp['Content-Type'] = mime
|
||||
resp['Content-Disposition'] = 'inline; filename="{}"'.format(name)
|
||||
resp._csp_ignore = True
|
||||
return resp
|
||||
|
||||
@@ -67,7 +67,12 @@ class EventList(PaginationMixin, ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.user.get_events_with_any_permission(self.request).prefetch_related(
|
||||
'organizer', '_settings_objects', 'organizer___settings_objects', 'organizer__meta_properties',
|
||||
'organizer',
|
||||
'organizer__sales_channels',
|
||||
'_settings_objects',
|
||||
'organizer___settings_objects',
|
||||
'organizer__meta_properties',
|
||||
'limit_sales_channels',
|
||||
Prefetch(
|
||||
'meta_values',
|
||||
EventMetaValue.objects.select_related('property'),
|
||||
|
||||
@@ -710,22 +710,26 @@ class OrderDownload(AsyncAction, OrderView):
|
||||
resp = HttpResponseRedirect(value.file.file.read())
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(value.file.file, content_type=value.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
|
||||
self.output.identifier, value.extension
|
||||
return FileResponse(
|
||||
value.file.file,
|
||||
filename='{}-{}-{}-{}{}'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
|
||||
self.output.identifier, value.extension
|
||||
),
|
||||
content_type=value.type
|
||||
)
|
||||
return resp
|
||||
elif isinstance(value, CachedCombinedTicket):
|
||||
if value.type == 'text/uri-list':
|
||||
resp = HttpResponseRedirect(value.file.file.read())
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(value.file.file, content_type=value.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
|
||||
return FileResponse(
|
||||
value.file.file,
|
||||
filename='{}-{}-{}{}'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
|
||||
),
|
||||
content_type=value.type
|
||||
)
|
||||
return resp
|
||||
else:
|
||||
return redirect(self.get_self_url())
|
||||
|
||||
@@ -1831,15 +1835,15 @@ class InvoiceDownload(EventPermissionRequiredMixin, View):
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
try:
|
||||
resp = FileResponse(self.invoice.file.file, content_type='application/pdf')
|
||||
return FileResponse(
|
||||
self.invoice.file.file,
|
||||
filename='{}.pdf'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number)),
|
||||
content_type='application/pdf'
|
||||
)
|
||||
except FileNotFoundError:
|
||||
invoice_pdf_task.apply(args=(self.invoice.pk,))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number))
|
||||
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
|
||||
return resp
|
||||
|
||||
|
||||
class OrderExtend(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
@@ -2421,9 +2425,9 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
|
||||
with language(order.locale, self.request.event.settings.region):
|
||||
email_context = get_email_context(event=order.event, order=order)
|
||||
email_template = LazyI18nString(form.cleaned_data['message'])
|
||||
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
|
||||
email_content = render_mail(email_template, email_context)
|
||||
if self.request.POST.get('action') == 'preview':
|
||||
email_subject = format_map(form.cleaned_data['subject'], email_context)
|
||||
email_content = render_mail(email_template, email_context)
|
||||
self.preview_output = {
|
||||
'subject': mark_safe(_('Subject: {subject}').format(
|
||||
subject=prefix_subject(order.event, escape(email_subject), highlight=True)
|
||||
@@ -2485,9 +2489,9 @@ class OrderPositionSendMail(OrderSendMail):
|
||||
with language(position.order.locale, self.request.event.settings.region):
|
||||
email_context = get_email_context(event=position.order.event, order=position.order, position=position)
|
||||
email_template = LazyI18nString(form.cleaned_data['message'])
|
||||
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
|
||||
email_content = render_mail(email_template, email_context)
|
||||
if self.request.POST.get('action') == 'preview':
|
||||
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
|
||||
email_content = render_mail(email_template, email_context)
|
||||
self.preview_output = {
|
||||
'subject': mark_safe(_('Subject: {subject}').format(
|
||||
subject=prefix_subject(position.order.event, escape(email_subject), highlight=True))
|
||||
|
||||
@@ -207,6 +207,7 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
||||
'organizer').prefetch_related(
|
||||
'organizer', '_settings_objects', 'organizer___settings_objects',
|
||||
'organizer__meta_properties',
|
||||
'limit_sales_channels',
|
||||
Prefetch(
|
||||
'meta_values',
|
||||
EventMetaValue.objects.select_related('property'),
|
||||
@@ -237,6 +238,7 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
||||
self.filter_form['meta_{}'.format(p.name)] for p in
|
||||
self.organizer.meta_properties.filter(filter_allowed=True)
|
||||
]
|
||||
ctx['sales_channels'] = self.request.organizer.sales_channels.all()
|
||||
return ctx
|
||||
|
||||
|
||||
|
||||
@@ -263,12 +263,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
|
||||
resp = HttpResponse(data, content_type=mimet)
|
||||
ftype = fname.split(".")[-1]
|
||||
if settings.DEBUG:
|
||||
# attachment is more secure as we're dealing with user-generated stuff here, but inline is much more convenient during debugging
|
||||
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
|
||||
resp._csp_ignore = True
|
||||
else:
|
||||
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
|
||||
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
|
||||
return resp
|
||||
elif "data" in request.POST:
|
||||
if cf:
|
||||
@@ -292,6 +287,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
ctx['layout'] = json.dumps(self.get_current_layout())
|
||||
ctx['title'] = self.title
|
||||
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
|
||||
ctx['maxfilesize'] = self.maxfilesize
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -308,6 +304,5 @@ class FontsCSSView(TemplateView):
|
||||
class PdfView(TemplateView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
|
||||
resp = FileResponse(cf.file, content_type='application/pdf')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
|
||||
resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf')
|
||||
return resp
|
||||
|
||||
@@ -131,7 +131,7 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
|
||||
elif v.quota:
|
||||
prod = _('Any product in quota "{quota}"').format(quota=str(v.quota.name))
|
||||
else:
|
||||
prod = _('Any product')
|
||||
prod = ""
|
||||
row = [
|
||||
v.code,
|
||||
v.valid_until.isoformat() if v.valid_until else "",
|
||||
|
||||
@@ -7,7 +7,7 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
|
||||
"PO-Revision-Date: 2026-03-03 20:00+0000\n"
|
||||
"PO-Revision-Date: 2026-03-14 22:00+0000\n"
|
||||
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
|
||||
"\n"
|
||||
@@ -16,7 +16,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.16.1\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -27734,7 +27734,7 @@ msgstr "Toon accountgeschiedenis"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/user/staff_session_edit.html:4
|
||||
msgid "Staff session"
|
||||
msgstr "Personeelssessie"
|
||||
msgstr "Beheerderssessie"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/user/staff_session_edit.html:6
|
||||
msgid "Session notes"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
|
||||
"PO-Revision-Date: 2026-03-02 10:00+0000\n"
|
||||
"PO-Revision-Date: 2026-03-14 22:00+0000\n"
|
||||
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
|
||||
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix/nl_Informal/>\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.16.1\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -27796,7 +27796,7 @@ msgstr "Toon accountgeschiedenis"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/user/staff_session_edit.html:4
|
||||
msgid "Staff session"
|
||||
msgstr "Personeelssessie"
|
||||
msgstr "Beheerderssessie"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/user/staff_session_edit.html:6
|
||||
msgid "Session notes"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
|
||||
"PO-Revision-Date: 2025-01-04 01:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"PO-Revision-Date: 2026-03-11 00:00+0000\n"
|
||||
"Last-Translator: Demir Kaya <demir@indieturk.org>\n"
|
||||
"Language-Team: Turkish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"tr/>\n"
|
||||
"Language: tr\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.9.2\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -37,7 +37,7 @@ msgstr "Arapça"
|
||||
|
||||
#: pretix/_base_settings.py:91
|
||||
msgid "Basque"
|
||||
msgstr ""
|
||||
msgstr "Baskça"
|
||||
|
||||
#: pretix/_base_settings.py:92
|
||||
msgid "Catalan"
|
||||
@@ -57,7 +57,7 @@ msgstr "Çekce"
|
||||
|
||||
#: pretix/_base_settings.py:96
|
||||
msgid "Croatian"
|
||||
msgstr ""
|
||||
msgstr "Hırvatça"
|
||||
|
||||
#: pretix/_base_settings.py:97
|
||||
msgid "Danish"
|
||||
@@ -89,7 +89,7 @@ msgstr "Yunanca"
|
||||
|
||||
#: pretix/_base_settings.py:104
|
||||
msgid "Hebrew"
|
||||
msgstr ""
|
||||
msgstr "İbranice"
|
||||
|
||||
#: pretix/_base_settings.py:105
|
||||
msgid "Indonesian"
|
||||
@@ -101,7 +101,7 @@ msgstr "İtalyanca"
|
||||
|
||||
#: pretix/_base_settings.py:107
|
||||
msgid "Japanese"
|
||||
msgstr ""
|
||||
msgstr "Japonca"
|
||||
|
||||
#: pretix/_base_settings.py:108
|
||||
msgid "Latvian"
|
||||
@@ -109,7 +109,7 @@ msgstr "Letonca"
|
||||
|
||||
#: pretix/_base_settings.py:109
|
||||
msgid "Norwegian Bokmål"
|
||||
msgstr ""
|
||||
msgstr "Norveççe"
|
||||
|
||||
#: pretix/_base_settings.py:110
|
||||
msgid "Polish"
|
||||
@@ -145,7 +145,7 @@ msgstr "İspanyolca"
|
||||
|
||||
#: pretix/_base_settings.py:118
|
||||
msgid "Spanish (Latin America)"
|
||||
msgstr ""
|
||||
msgstr "İspanyolca (Latin Amerikan)"
|
||||
|
||||
#: pretix/_base_settings.py:119
|
||||
msgid "Turkish"
|
||||
@@ -203,10 +203,8 @@ msgid "Client secret"
|
||||
msgstr "Müşteri sırrı"
|
||||
|
||||
#: pretix/api/models.py:116
|
||||
#, fuzzy
|
||||
#| msgid "Enable"
|
||||
msgid "Enable webhook"
|
||||
msgstr "Etkinleştirme"
|
||||
msgstr "Webhook etkinleştir"
|
||||
|
||||
#: pretix/api/models.py:117
|
||||
#: pretix/control/templates/pretixcontrol/organizers/webhooks.html:36
|
||||
@@ -277,10 +275,9 @@ msgid "Unknown plugin: '{name}'."
|
||||
msgstr "Bilinmeyen eklenti: '{name}'."
|
||||
|
||||
#: pretix/api/serializers/event.py:286 pretix/api/serializers/organizer.py:88
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "Unknown plugin: '{name}'."
|
||||
#, python-brace-format
|
||||
msgid "Restricted plugin: '{name}'."
|
||||
msgstr "Bilinmeyen eklenti: '{name}'."
|
||||
msgstr "sınırlandırılmış eklenti: '{name}'."
|
||||
|
||||
#: pretix/api/serializers/item.py:87 pretix/api/serializers/item.py:149
|
||||
#: pretix/api/serializers/item.py:405
|
||||
@@ -612,10 +609,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/webhooks.py:413
|
||||
#, fuzzy
|
||||
#| msgid "Shop not live"
|
||||
msgid "Shop taken live"
|
||||
msgstr "Mağaza kapalı"
|
||||
msgstr "Mağaza çevrimdışı"
|
||||
|
||||
#: pretix/api/webhooks.py:417
|
||||
#, fuzzy
|
||||
@@ -624,16 +619,12 @@ msgid "Shop taken offline"
|
||||
msgstr "Mağaza çevrimdışı duruma getirildi."
|
||||
|
||||
#: pretix/api/webhooks.py:421
|
||||
#, fuzzy
|
||||
#| msgid "The order has been created."
|
||||
msgid "Test-Mode of shop has been activated"
|
||||
msgstr "Sipariş oluşturuldu."
|
||||
msgstr "Mağazanın Test-Modu aktive oldu"
|
||||
|
||||
#: pretix/api/webhooks.py:425
|
||||
#, fuzzy
|
||||
#| msgid "The order has been created."
|
||||
msgid "Test-Mode of shop has been deactivated"
|
||||
msgstr "Sipariş oluşturuldu."
|
||||
msgstr "Mağazanın Test-Modu deaktif oldu"
|
||||
|
||||
#: pretix/api/webhooks.py:429
|
||||
#, fuzzy
|
||||
@@ -660,10 +651,8 @@ msgid "Waiting list entry received voucher"
|
||||
msgstr "Liste girdileri bekleniyor"
|
||||
|
||||
#: pretix/api/webhooks.py:445
|
||||
#, fuzzy
|
||||
#| msgid "Voucher code"
|
||||
msgid "Voucher added"
|
||||
msgstr "Kupon kodu"
|
||||
msgstr "Kupon kodu eklendi"
|
||||
|
||||
#: pretix/api/webhooks.py:449
|
||||
#, fuzzy
|
||||
|
||||
@@ -38,7 +38,7 @@ var pretixpaypal = {
|
||||
credit: gettext('PayPal Credit'),
|
||||
card: gettext('Credit Card'),
|
||||
paylater: gettext('PayPal Pay Later'),
|
||||
ideal: gettext('iDEAL'),
|
||||
ideal: gettext('iDEAL | Wero'),
|
||||
sepa: gettext('SEPA Direct Debit'),
|
||||
bancontact: gettext('Bancontact'),
|
||||
giropay: gettext('giropay'),
|
||||
|
||||
@@ -38,13 +38,10 @@ from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Event, InvoiceAddress, Order, User,
|
||||
)
|
||||
from pretix.base.models import Checkin, Event, InvoiceAddress, Order, User
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.format import format_map
|
||||
|
||||
|
||||
def _chunks(lst, n):
|
||||
@@ -64,7 +61,6 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
user = User.objects.get(pk=user) if user else None
|
||||
subject = LazyI18nString(subject)
|
||||
message = LazyI18nString(message)
|
||||
attachments_for_log = [cf.filename for cf in CachedFile.objects.filter(pk__in=attachments)] if attachments else []
|
||||
|
||||
def _send_to_order(o):
|
||||
send_to_order = recipients in ('both', 'orders')
|
||||
@@ -122,7 +118,7 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
|
||||
with language(o.locale, event.settings.region):
|
||||
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
|
||||
mail(
|
||||
outgoing_mail = mail(
|
||||
p.attendee_email,
|
||||
subject,
|
||||
message,
|
||||
@@ -135,25 +131,17 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
attach_ical=attach_ical,
|
||||
attach_cached_files=attachments
|
||||
)
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent.attendee',
|
||||
user=user,
|
||||
data={
|
||||
'position': p.positionid,
|
||||
'subject': format_map(subject.localize(o.locale), email_context),
|
||||
'message': format_map(message.localize(o.locale), email_context),
|
||||
'recipient': p.attendee_email,
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': attachments_for_log,
|
||||
}
|
||||
)
|
||||
if outgoing_mail:
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent.attendee',
|
||||
user=user,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
|
||||
if send_to_order and o.email:
|
||||
with language(o.locale, event.settings.region):
|
||||
email_context = get_email_context(event=event, order=o, invoice_address=ia)
|
||||
mail(
|
||||
outgoing_mail = mail(
|
||||
o.email,
|
||||
subject,
|
||||
message,
|
||||
@@ -165,19 +153,12 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
attach_ical=attach_ical,
|
||||
attach_cached_files=attachments,
|
||||
)
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent',
|
||||
user=user,
|
||||
data={
|
||||
'subject': format_map(subject.localize(o.locale), email_context),
|
||||
'message': format_map(message.localize(o.locale), email_context),
|
||||
'recipient': o.email,
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': attachments_for_log,
|
||||
}
|
||||
)
|
||||
if outgoing_mail:
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent',
|
||||
user=user,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
|
||||
for chunk in _chunks(objects, 1000):
|
||||
orders = Order.objects.filter(pk__in=chunk, event=event)
|
||||
|
||||
@@ -92,70 +92,89 @@ logger = logging.getLogger('pretix.plugins.stripe')
|
||||
# State of the payment methods
|
||||
#
|
||||
# Source: https://stripe.com/docs/payments/payment-methods/overview
|
||||
# Last Update: 2023-12-20
|
||||
# Last Update: 2026-06-12
|
||||
#
|
||||
# pretix Staff: Do not forget to enable/"On by default" newly added payment methods in
|
||||
# Stripe's managed payment methods configuration for the Stripe/pretix.eu connect platform.
|
||||
#
|
||||
# The categorization and order of payment methods is based on the list of the managed payment method configration
|
||||
# in the Stripe Dashboard.
|
||||
|
||||
# Cards
|
||||
# - Credit and Debit Cards: ✓
|
||||
# - Apple, Google Pay: ✓
|
||||
# * Cartes Bancaires: ✓
|
||||
# * Korean cards: ✓
|
||||
# * Japan installments: ✗
|
||||
# * JCB: ✓
|
||||
# * Meses sin intereses: ✗
|
||||
#
|
||||
# Bank debits
|
||||
# - ACH Debit: ✗
|
||||
# - Canadian PADs: ✗
|
||||
# - BACS Direct Debit: ✗
|
||||
# - SEPA Direct Debit: ✓
|
||||
# - BECS Direct Debit: ✗
|
||||
# Wallets
|
||||
# - Apple: ✓ (Cards)
|
||||
# - Google Pay: ✓ (Cards)
|
||||
# - Link: ✓ (PaymentRequestButton/Cards)
|
||||
# - Alipay: ✓
|
||||
# - Stablecoins and Crypto: ✗
|
||||
# - Kakao Pay: ✗
|
||||
# - Naver Pay: ✗
|
||||
# - MB Way: ✗
|
||||
# - Satis Pay: ✗
|
||||
# - WeChat Pay: ✓
|
||||
# - PAYCO: ✗
|
||||
# - PayPal: ✓ (No settings UI yet; incompatible with Connect+Direct Charges)
|
||||
# - Samsung pay: ✗
|
||||
# - MobilePay: ✓
|
||||
# - Revolut Pay: ✓
|
||||
# - Amazon Pay: ✗
|
||||
# - PayPay: ✗
|
||||
# - GrabPay: ✗
|
||||
# - Cash App Pay: ✗
|
||||
# - Secure Remote Commerce: ✗
|
||||
#
|
||||
# Bank redirects
|
||||
# - Bancontact: ✓
|
||||
# - BLIK: ✗
|
||||
# - EPS: ✓
|
||||
# - giropay: (deprecated)
|
||||
# - iDEAL: ✓
|
||||
# - P24: ✓
|
||||
# - Sofort: (deprecated)
|
||||
# - FPX: ✗
|
||||
# - PayNow: ✗
|
||||
# - UPI: ✗
|
||||
# - Netbanking: ✗
|
||||
# - Przelewy24: ✓
|
||||
# - BLIK: ✗
|
||||
# - Pay By Bank: ✓
|
||||
# - TWINT: ✓
|
||||
# - Wero: ✓ (No settings UI yet)
|
||||
#
|
||||
# Bank transfers
|
||||
# - ACH Bank Transfer: ✗
|
||||
# - SEPA Bank Transfer: ✗
|
||||
# - UK Bank Transfer: ✗
|
||||
# - Multibanco: ✗
|
||||
# - Furikomi (Japan): ✗
|
||||
# - Mexico Bank Transfer: ✗
|
||||
# - giropay: ✗ (deprecated)
|
||||
# - Sofort: ✗ (deprecated)
|
||||
#
|
||||
# Buy now, pay later
|
||||
# - Affirm: ✓
|
||||
# - Afterpay/Clearpay: ✗
|
||||
# - Billie: ✗
|
||||
# - Klarna: ✓
|
||||
# - Afterpay/Clearpay: ✗
|
||||
# - Zip: ✗
|
||||
# - Alma: ✗
|
||||
# - Affirm: ✓
|
||||
#
|
||||
# Bank debits
|
||||
# - SEPA Direct Debit: ✓
|
||||
# - ACH Direct Debit: ✗
|
||||
# - Australian BECS Direct Debit: ✗
|
||||
# - Canadian pre-authorized debits: ✗
|
||||
# - BACS Direct Debit: ✗
|
||||
# - FPX: ✗
|
||||
# - NZ BECS Direct Debit: ✗
|
||||
#
|
||||
# Bank transfers
|
||||
# - Bank Transfer: ✗
|
||||
#
|
||||
# Vouchers
|
||||
# - Multibanco: ✓
|
||||
# - Boleto: ✗
|
||||
# - Konbini: ✗
|
||||
# - OXXO: ✗
|
||||
#
|
||||
# Real-time payments
|
||||
# - Swish: ✓
|
||||
# - UPI: ✗
|
||||
# - Pix: ✗
|
||||
# - PayTo: ✗
|
||||
# - PayNow: ✗
|
||||
# - PromptPay: ✓
|
||||
# - Pix: ✗
|
||||
#
|
||||
# Vouchers
|
||||
# - Konbini: ✗
|
||||
# - OXXO: ✗
|
||||
# - Boleto: ✗
|
||||
#
|
||||
# Wallets
|
||||
# - Apple Pay: ✓ (Cards)
|
||||
# - Google Pay: ✓ (Cards)
|
||||
# - Secure Remote Commerce: ✗
|
||||
# - Link: ✓ (PaymentRequestButton)
|
||||
# - Cash App Pay: ✗
|
||||
# - PayPal: ✓ (No settings UI yet)
|
||||
# - MobilePay: ✓
|
||||
# - Alipay: ✓
|
||||
# - WeChat Pay: ✓
|
||||
# - GrabPay: ✓
|
||||
|
||||
|
||||
class StripeSettingsHolder(BasePaymentProvider):
|
||||
@@ -1550,7 +1569,7 @@ class StripeGiropay(StripeRedirectWithAccountNamePaymentIntentMethod):
|
||||
class StripeIdeal(StripeRedirectMethod):
|
||||
identifier = 'stripe_ideal'
|
||||
verbose_name = _('iDEAL via Stripe')
|
||||
public_name = _('iDEAL')
|
||||
public_name = _('iDEAL | Wero')
|
||||
method = 'ideal'
|
||||
explanation = _(
|
||||
'iDEAL is an online payment method available to customers of Dutch banks. Please keep your online '
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
{% csrf_token %}
|
||||
<div class="panel-group" id="questions_accordion">
|
||||
{% if invoice_address_asked or event.settings.invoice_name_required %}
|
||||
{% if invoice_address_asked and not request.GET.generate_invoice == "true" and not event.settings.invoice_reissue_after_modify %}
|
||||
{% if invoice_address_asked and not request.GET.generate_invoice == "true" and not invoice_generation_selfservice %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
Modifying your invoice address will not automatically generate a new invoice.
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
{% bootstrap_form_errors timemachine_form "all" %}
|
||||
|
||||
<p>{% trans "Test your shop as if it were a different date and time." %}</p>
|
||||
<p>{% trans "Please note that the changed time is not taken into account for aspects of the shop that affect quotas, such as the validity period of carts and vouchers." %}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@@ -44,4 +45,4 @@
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -841,9 +841,13 @@ class AnswerDownload(EventViewMixin, View):
|
||||
return Http404()
|
||||
|
||||
ftype, _ = mimetypes.guess_type(answer.file.name)
|
||||
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-cart-{}"'.format(
|
||||
filename = '{}-cart-{}'.format(
|
||||
self.request.event.slug.upper(),
|
||||
os.path.basename(answer.file.name).split('.', 1)[1]
|
||||
).encode("ascii", "ignore")
|
||||
resp = FileResponse(
|
||||
answer.file,
|
||||
filename=filename,
|
||||
content_type=ftype or 'application/binary'
|
||||
)
|
||||
return resp
|
||||
|
||||
@@ -909,6 +909,21 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
|
||||
def get(self, request, *args, **kwargs):
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
ctx['invoice_generation_selfservice'] = (
|
||||
self.request.event.settings.invoice_reissue_after_modify or
|
||||
(
|
||||
can_generate_invoice(self.request.event, self.order, ignore_payments=True) and
|
||||
not self.order.invoices.exists()
|
||||
)
|
||||
)
|
||||
|
||||
return ctx
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
self.kwargs = kwargs
|
||||
@@ -1205,30 +1220,25 @@ class OrderDownloadMixin:
|
||||
resp = HttpResponseRedirect(value.file.file.read())
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(value.file.file, content_type=value.type)
|
||||
if self.order_position.subevent:
|
||||
# Subevent date in filename improves accessibility e.g. for screen reader users
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
|
||||
self.order_position.subevent.date_from.strftime('%Y_%m_%d'),
|
||||
self.output.identifier, value.extension
|
||||
)
|
||||
else:
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
|
||||
self.output.identifier, value.extension
|
||||
)
|
||||
return resp
|
||||
name_parts = (
|
||||
self.request.event.slug.upper(),
|
||||
self.order.code,
|
||||
str(self.order_position.positionid),
|
||||
self.order_position.subevent.date_from.strftime('%Y_%m_%d') if self.order_position.subevent else None,
|
||||
self.output.identifier
|
||||
)
|
||||
filename = "-".join(filter(None, name_parts)) + value.extension
|
||||
return FileResponse(value.file.file, filename=filename, content_type=value.type)
|
||||
elif isinstance(value, CachedCombinedTicket):
|
||||
if value.type == 'text/uri-list':
|
||||
resp = HttpResponseRedirect(value.file.file.read())
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(value.file.file, content_type=value.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
|
||||
return FileResponse(
|
||||
value.file.file,
|
||||
filename="-".join(self.request.event.slug.upper(), self.order.code, self.output.identifier) + value.extension,
|
||||
content_type=value.type
|
||||
)
|
||||
return resp
|
||||
else:
|
||||
return redirect(self.get_self_url())
|
||||
|
||||
@@ -1368,13 +1378,14 @@ class InvoiceDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
try:
|
||||
resp = FileResponse(invoice.file.file, content_type='application/pdf')
|
||||
return FileResponse(
|
||||
invoice.file.file,
|
||||
filename='{}.pdf'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", invoice.number)),
|
||||
content_type='application/pdf'
|
||||
)
|
||||
except FileNotFoundError:
|
||||
invoice_pdf_task.apply(args=(invoice.pk,))
|
||||
return self.get(request, *args, **kwargs)
|
||||
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", invoice.number))
|
||||
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
|
||||
return resp
|
||||
|
||||
|
||||
class OrderChangeMixin:
|
||||
|
||||
@@ -173,8 +173,8 @@ $(function () {
|
||||
if (!dependents.transmission_peppol_participant_id.val()) {
|
||||
const fill_peppol_id = function () {
|
||||
const vatId = dependents.vat_id.val();
|
||||
if (vatId && vatId.startsWith("BE") && dependents.transmission_type.val() === "peppol" && autofill_peppol_id) {
|
||||
dependents.transmission_peppol_participant_id.val("0201:" + vatId.substring(2))
|
||||
if (vatId && vatId.startsWith("BE") && dependents.transmission_type.val() === "peppol") {
|
||||
dependents.transmission_peppol_participant_id.val("0208:" + vatId.substring(2).replaceAll(".", ""))
|
||||
}
|
||||
}
|
||||
dependents.vat_id.add(dependents.transmission_type).on("change", fill_peppol_id);
|
||||
|
||||
@@ -35,8 +35,8 @@ from django_scopes import scopes_disabled
|
||||
from tests.const import SAMPLE_PNG
|
||||
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, Item, Order, OrderPosition, Organizer, Question,
|
||||
SeatingPlan,
|
||||
GiftCard, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
|
||||
Organizer, Question, SeatingPlan,
|
||||
)
|
||||
from pretix.base.models.orders import CartPosition, OrderFee, QuestionAnswer
|
||||
|
||||
@@ -3371,3 +3371,251 @@ def test_order_create_rounding_default_pretixpos_fallback(device, device_client,
|
||||
assert resp.data["total"] == "500.00"
|
||||
assert resp.data["positions"][0]["price"] == "100.00"
|
||||
assert resp.data["positions"][-1]["price"] == "100.00"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"order_status,status_code",
|
||||
[
|
||||
(
|
||||
Order.STATUS_PENDING, 201
|
||||
),
|
||||
(
|
||||
Order.STATUS_PAID, 400
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_use_gift_cards_only_pending(token_client, organizer, event, item, quota, question, order_status, status_code):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
with scopes_disabled():
|
||||
customer = organizer.customers.create()
|
||||
res['customer'] = customer.identifier
|
||||
res['api_meta'] = {
|
||||
'test': 1
|
||||
}
|
||||
|
||||
gc = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||
gc.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
|
||||
|
||||
res['status'] = order_status
|
||||
del res['payment_provider']
|
||||
res['use_gift_cards'] = [gc.secret]
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == status_code
|
||||
if status_code != 201:
|
||||
assert resp.data == {'use_gift_cards': ['The attribute use_gift_cards is only supported for orders that are created as pending']}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"send_mail,mail_amount",
|
||||
[
|
||||
(
|
||||
False, 0
|
||||
),
|
||||
(
|
||||
True, 2
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_order_create_use_gift_card(token_client, organizer, event, item, quota, question, send_mail, mail_amount):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
with scopes_disabled():
|
||||
customer = organizer.customers.create()
|
||||
|
||||
res['customer'] = customer.identifier
|
||||
res['api_meta'] = {
|
||||
'test': 1
|
||||
}
|
||||
|
||||
if send_mail:
|
||||
res['send_email'] = True
|
||||
|
||||
gc = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||
gc.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
|
||||
del res['payment_provider']
|
||||
res['use_gift_cards'] = [gc.secret]
|
||||
|
||||
djmail.outbox = []
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
assert o.status == Order.STATUS_PAID
|
||||
|
||||
assert gc.transactions.count() == 2
|
||||
assert -gc.transactions.last().value == o.total
|
||||
|
||||
assert len(djmail.outbox) == mail_amount
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_use_multiple_gift_cards(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
with scopes_disabled():
|
||||
customer = organizer.customers.create()
|
||||
res['customer'] = customer.identifier
|
||||
res['api_meta'] = {
|
||||
'test': 1
|
||||
}
|
||||
del res['payment_provider']
|
||||
|
||||
gc_one_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||
gc_one_eur.transactions.create(value=Decimal("1.00"), acceptor=organizer).save()
|
||||
|
||||
gc_empty = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||
|
||||
gc_wrong_currency = GiftCard.objects.create(issuer=organizer, currency='USD')
|
||||
gc_wrong_currency.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
|
||||
|
||||
gc_enough_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||
gc_enough_eur.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
|
||||
|
||||
res['use_gift_cards'] = [gc_one_eur.secret, gc_empty.secret, gc_wrong_currency.secret, gc_enough_eur.secret]
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
# order has a payment entry per giftcard
|
||||
assert o.status == Order.STATUS_PAID
|
||||
assert o.payments.count() == 4
|
||||
|
||||
assert gc_one_eur.transactions.count() == 2 # +1€ charge and -1€ payment
|
||||
assert o.payments.all()[0].state == OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
assert Decimal(-1.00) == gc_one_eur.transactions.last().value
|
||||
|
||||
assert gc_empty.transactions.count() == 0 # no charge and no payment transaction
|
||||
assert o.payments.all()[1].state == OrderPayment.PAYMENT_STATE_FAILED
|
||||
|
||||
assert gc_wrong_currency.transactions.count() == 1 # charge transaction
|
||||
assert o.payments.all()[2].state == OrderPayment.PAYMENT_STATE_FAILED
|
||||
|
||||
assert gc_enough_eur.transactions.count() == 2 # +100€ charge and -remainder € payment
|
||||
assert o.payments.all()[3].state == OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
assert -(o.total - Decimal(1.00)) == gc_enough_eur.transactions.last().value
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_use_gift_card_exclusive_with_payment_provider(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
with scopes_disabled():
|
||||
customer = organizer.customers.create()
|
||||
res['customer'] = customer.identifier
|
||||
res['api_meta'] = {
|
||||
'test': 1
|
||||
}
|
||||
gc_value = Decimal("1.00")
|
||||
gc = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||
gc.transactions.create(value=gc_value, acceptor=organizer).save()
|
||||
|
||||
res['use_gift_cards'] = [gc.secret]
|
||||
|
||||
res_with_payment_provider = copy.deepcopy(res)
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res_with_payment_provider
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.json() == {"use_gift_cards": ["The attribute use_gift_cards is not compatible with payment_provider or payment_info"]}
|
||||
|
||||
res_with_payment_info = copy.deepcopy(res)
|
||||
res_with_payment_info['payment_info'] = {"a": "b"}
|
||||
del res_with_payment_info['payment_provider']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res_with_payment_info
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.json() == {"use_gift_cards": ["The attribute use_gift_cards is not compatible with payment_provider or payment_info"]}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_use_gift_card_repeated(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
with scopes_disabled():
|
||||
customer = organizer.customers.create()
|
||||
res['customer'] = customer.identifier
|
||||
res['api_meta'] = {
|
||||
'test': 1
|
||||
}
|
||||
del res['payment_provider']
|
||||
|
||||
gc_one_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||
gc_one_eur.transactions.create(value=Decimal("1.00"), acceptor=organizer).save()
|
||||
|
||||
gc_enough_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||
gc_enough_eur.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
|
||||
|
||||
res['use_gift_cards'] = [gc_one_eur.secret, gc_one_eur.secret, gc_enough_eur.secret]
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.json() == {'use_gift_cards': ['Multiple copies of the same gift card secret are not allowed']}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_use_gift_card_invalid_secret(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
with scopes_disabled():
|
||||
customer = organizer.customers.create()
|
||||
|
||||
res['customer'] = customer.identifier
|
||||
res['api_meta'] = {
|
||||
'test': 1
|
||||
}
|
||||
del res['payment_provider']
|
||||
|
||||
gc_enough_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||
gc_enough_eur.transactions.create(value=Decimal("100.00"),
|
||||
acceptor=organizer).save()
|
||||
|
||||
res['use_gift_cards'] = ["INVALID", gc_enough_eur.secret]
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
assert o.status == Order.STATUS_PAID
|
||||
assert o.payments.count() == 2
|
||||
assert o.payments.all()[0].state == OrderPayment.PAYMENT_STATE_FAILED
|
||||
assert o.payments.all()[1].state == OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
|
||||
Reference in New Issue
Block a user