Compare commits

...

28 Commits

Author SHA1 Message Date
Richard Schreiber
c75c32c4fe do not handle async signals at all 2026-03-17 15:08:23 +01:00
Raphael Michel
c5282364cb Upgrade python versions in CI 2026-03-17 14:06:37 +01:00
Raphael Michel
4f2b6246ce Upgrade min python to 3.11 2026-03-17 14:05:16 +01:00
Raphael Michel
a0d066b007 Replace pytz with zoneinfo 2026-03-17 12:10:06 +01:00
Raphael Michel
4f2b565ab5 Patch around GeoIP issue 2026-03-17 12:10:05 +01:00
Phin Wolkwitz
f4a775ffe7 Update model index migrations (Meta.index_together removed in Django 5.2) 2026-03-17 12:09:38 +01:00
Phin Wolkwitz
a452701414 Fix migration monkeypatching 2026-03-17 12:09:38 +01:00
Phin Wolkwitz
946e7f5b88 Update pytest filterwarnings, sort imports 2026-03-17 12:09:37 +01:00
Phin Wolkwitz
9da44f1e74 Update django to 5.2, initial fixes 2026-03-17 12:09:37 +01:00
George Hickman
dfaa4c3359 Add session_login function (#5955)
* Add session_login function

* Make helper do more things and use it

---------

Co-authored-by: Raphael Michel <michel@rami.io>
2026-03-16 17:39:04 +01:00
Richard Schreiber
ed1966bc96 Improve autofill for peppol BE (Z#23224796) (#5992) 2026-03-16 10:48:05 +01:00
Ruud Hendrickx
fad5284f25 Translations: Update Dutch (Belgium)
Currently translated at 75.7% (4741 of 6257 strings)

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

powered by weblate
2026-03-16 10:47:43 +01:00
Ruud Hendrickx
f57530d3ff Translations: Update Dutch (Belgium)
Currently translated at 75.6% (4736 of 6257 strings)

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

powered by weblate
2026-03-16 10:47:43 +01:00
Ruud Hendrickx
1427edf5ab Translations: Update Dutch (Belgium)
Currently translated at 74.7% (4676 of 6257 strings)

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

powered by weblate
2026-03-16 10:47:43 +01:00
Ruud Hendrickx
4898475d56 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-16 10:47:43 +01:00
Ruud Hendrickx
cdacc84553 Translations: Update Dutch
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-16 10:47:43 +01:00
Ruud Hendrickx
ef483d5229 Translations: Update Dutch (Belgium)
Currently translated at 73.0% (4570 of 6257 strings)

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

powered by weblate
2026-03-16 10:47:43 +01:00
Pedro Orlando
ad6f5a7b54 Translations: Update Portuguese (Brazil)
Currently translated at 95.6% (5985 of 6257 strings)

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

powered by weblate
2026-03-16 10:47:43 +01:00
Demir Kaya
ecc49d453d Translations: Update Turkish
Currently translated at 39.7% (2488 of 6257 strings)

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

powered by weblate
2026-03-16 10:47:43 +01:00
Pedro Orlando
c45070b190 Translations: Update Portuguese (Brazil)
Currently translated at 95.2% (5957 of 6257 strings)

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

powered by weblate
2026-03-16 10:47:43 +01:00
Ruud Hendrickx
aea2a1ca10 Translations: Update Dutch (Belgium)
Currently translated at 72.6% (4548 of 6257 strings)

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

powered by weblate
2026-03-16 10:47:43 +01:00
Lukas Bockstaller
d791b9e108 fix rst (#5993) 2026-03-16 09:37:45 +01:00
dependabot[bot]
2c9802d1cb Update pyjwt requirement from ==2.11.* to ==2.12.* (#5984)
Updates the requirements on [pyjwt](https://github.com/jpadilla/pyjwt) to permit the latest version.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.11.0...2.12.0)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-version: 2.12.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 09:27:49 +01:00
Lukas Bockstaller
c39f1bfcc2 handle gift card payment via create order api endpoint (Z#23224691) (#5968)
* adds safeguard to prevent empty giftcard transactions on giftcards of value 0.00

* implement giftcard payment via order create

* styling

* let create_transactions() handle all the mailing

* docs

* provide more context for failed transactions

* documentation lectoring

* reject duplicate gift card secrets

* make payment_provider and use_gift_cards exclusive

* handle unknown gift cards

* Apply suggestion from @pajowu

Co-authored-by: pajowu <engelhardt@pretix.eu>

* Update src/pretix/control/templates/pretixcontrol/giftcards/payment.html

Co-authored-by: pajowu <engelhardt@pretix.eu>

---------

Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-03-16 08:51:27 +01:00
Richard Schreiber
894128deab Fix log-display for team.invite.deleted (#5988) 2026-03-16 08:21:45 +01:00
luelista
3352ee2bbe Limits of the time machine feature (Z#23212144) (#5952)
* Add note about limits of the time machine feature
* Always check voucher validity against real time, not time machine time
2026-03-12 18:09:16 +01:00
Martin Gross
af28785fb9 Stripe: iDEAL -> iDEAL | Wero rebrand 2026-03-12 13:37:35 +01:00
Martin Gross
54e4957e89 Stripe: Update list of supported payment methods 2026-03-12 13:37:06 +01:00
38 changed files with 1238 additions and 529 deletions

View File

@@ -24,7 +24,7 @@ jobs:
name: Packaging
strategy:
matrix:
python-version: ["3.11"]
python-version: ["3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}

View File

@@ -24,10 +24,10 @@ jobs:
name: Check gettext syntax
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- uses: actions/cache@v4
with:
path: ~/.cache/pip
@@ -49,10 +49,10 @@ jobs:
name: Spellcheck
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- uses: actions/cache@v4
with:
path: ~/.cache/pip

View File

@@ -24,10 +24,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- uses: actions/cache@v4
with:
path: ~/.cache/pip
@@ -44,10 +44,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- uses: actions/cache@v4
with:
path: ~/.cache/pip
@@ -64,10 +64,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.13
- name: Install Dependencies
run: pip3 install licenseheaders
- name: Run licenseheaders

View File

@@ -23,13 +23,15 @@ jobs:
name: Tests
strategy:
matrix:
python-version: ["3.10", "3.11", "3.13"]
python-version: ["3.11", "3.13", "3.14"]
database: [sqlite, postgres]
exclude:
- database: sqlite
python-version: "3.10"
- database: sqlite
python-version: "3.11"
- database: sqlite
python-version: "3.12"
services:
postgres:
image: postgres:15
@@ -81,4 +83,4 @@ jobs:
file: src/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
if: matrix.database == 'postgres' && matrix.python-version == '3.13'

View File

@@ -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

View File

@@ -3,7 +3,7 @@ name = "pretix"
dynamic = ["version"]
description = "Reinventing presales, one ticket at a time"
readme = "README.rst"
requires-python = ">=3.10"
requires-python = ">=3.11"
license = {file = "LICENSE"}
keywords = ["tickets", "web", "shop", "ecommerce"]
authors = [
@@ -19,10 +19,11 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Environment :: Web Environment",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Framework :: Django :: 4.2",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Framework :: Django :: 5.2",
]
dependencies = [
@@ -36,7 +37,7 @@ dependencies = [
"css-inline==0.20.*",
"defusedcsv>=1.1.0",
"dnspython==2.*",
"Django[argon2]==4.2.*,>=4.2.26",
"Django[argon2]==5.2.*",
"django-bootstrap3==26.1",
"django-compressor==4.6.0",
"django-countries==8.2.*",
@@ -73,7 +74,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",

View File

@@ -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()

View File

@@ -45,7 +45,6 @@ import pycountry
from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.gis.geoip2 import GeoIP2
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.validators import (
@@ -102,6 +101,7 @@ from pretix.helpers.countries import (
from pretix.helpers.escapejson import escapejson_attr
from pretix.helpers.http import get_client_ip
from pretix.helpers.i18n import get_format_without_seconds
from pretix.helpers.security import get_geoip
from pretix.presale.signals import question_form_fields
logger = logging.getLogger(__name__)
@@ -393,7 +393,7 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def guess_country_from_request(request, event):
if settings.HAS_GEOIP:
g = GeoIP2()
g = get_geoip()
try:
res = g.country(get_client_ip(request))
if res['country_code'] and len(res['country_code']) == 2:

View File

@@ -36,8 +36,9 @@ from django.core.management.commands.makemigrations import Command as Parent
from ._migrations import monkeypatch_migrations
monkeypatch_migrations()
class Command(Parent):
pass
def handle(self, *args, **kwargs):
monkeypatch_migrations()
return super().handle(*args, **kwargs)

View File

@@ -41,16 +41,20 @@ class Migration(migrations.Migration):
name='datetime',
field=models.DateTimeField(),
),
migrations.AlterIndexTogether(
name='logentry',
index_together={('datetime', 'id')},
migrations.AddIndex(
'logentry',
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b1fe5a_idx"),
),
migrations.AlterIndexTogether(
name='order',
index_together={('datetime', 'id'), ('last_modified', 'id')},
migrations.AddIndex(
'order',
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
),
migrations.AlterIndexTogether(
name='transaction',
index_together={('datetime', 'id')},
migrations.AddIndex(
'order',
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
),
migrations.AddIndex(
'transaction',
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b20405_idx"),
),
]

View File

@@ -61,7 +61,10 @@ class Migration(migrations.Migration):
options={
'ordering': ('identifier', 'type', 'organizer'),
'unique_together': {('identifier', 'type', 'organizer')},
'index_together': {('identifier', 'type', 'organizer'), ('updated', 'id')},
'indexes': [
models.Index(fields=('identifier', 'type', 'organizer'), name='reusable_medium_organizer_index'),
models.Index(fields=('updated', 'id'), name="pretixbase__updated_093277_idx")
],
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),

View File

@@ -9,31 +9,6 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RenameIndex(
model_name="logentry",
new_name="pretixbase__datetim_b1fe5a_idx",
old_fields=("datetime", "id"),
),
migrations.RenameIndex(
model_name="order",
new_name="pretixbase__datetim_66aff0_idx",
old_fields=("datetime", "id"),
),
migrations.RenameIndex(
model_name="order",
new_name="pretixbase__last_mo_4ebf8b_idx",
old_fields=("last_modified", "id"),
),
migrations.RenameIndex(
model_name="reusablemedium",
new_name="pretixbase__updated_093277_idx",
old_fields=("updated", "id"),
),
migrations.RenameIndex(
model_name="transaction",
new_name="pretixbase__datetim_b20405_idx",
old_fields=("datetime", "id"),
),
migrations.AlterField(
model_name="attendeeprofile",
name="id",

View File

@@ -1,6 +1,6 @@
# Generated by Django 4.2.10 on 2024-04-02 15:16
from django.db import migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@@ -10,8 +10,8 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AlterIndexTogether(
name="reusablemedium",
index_together=set(),
migrations.RemoveIndex(
"reusablemedium",
'reusable_medium_organizer_index',
),
]

View File

@@ -88,7 +88,7 @@ class LogEntry(models.Model):
class Meta:
ordering = ('-datetime', '-id')
indexes = [models.Index(fields=["datetime", "id"])]
indexes = [models.Index(fields=["datetime", "id"], name="pretixbase__datetim_b1fe5a_idx")]
def display(self):
from pretix.base.logentrytype_registry import log_entry_types

View File

@@ -122,7 +122,7 @@ class ReusableMedium(LoggedModel):
class Meta:
unique_together = (("identifier", "type", "organizer"),)
indexes = [
models.Index(fields=("updated", "id")),
models.Index(fields=("updated", "id"), name="pretixbase__updated_093277_idx"),
]
ordering = "identifier", "type", "organizer"

View File

@@ -336,8 +336,8 @@ class Order(LockModel, LoggedModel):
verbose_name_plural = _("Orders")
ordering = ("-datetime", "-pk")
indexes = [
models.Index(fields=["datetime", "id"]),
models.Index(fields=["last_modified", "id"]),
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
]
constraints = [
models.UniqueConstraint(fields=["organizer", "code"], name="order_organizer_code_uniq"),
@@ -3080,7 +3080,7 @@ class Transaction(models.Model):
class Meta:
ordering = 'datetime', 'pk'
indexes = [
models.Index(fields=['datetime', 'id'])
models.Index(fields=['datetime', 'id'], name="pretixbase__datetim_b20405_idx")
]
def save(self, *args, **kwargs):

View File

@@ -1526,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
@@ -1550,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 {})
}
}
@@ -1627,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:
@@ -1656,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:

View File

@@ -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

View File

@@ -60,23 +60,25 @@ def _populate_app_cache():
def get_defining_app(o):
# If sentry packed this in a wrapper, unpack that
if "sentry" in o.__module__:
module = getattr(o, "__module__", None)
if module and "sentry" in module:
o = o.__wrapped__
if hasattr(o, "__mocked_app"):
return o.__mocked_app
# Find the Django application this belongs to
searchpath = o.__module__
searchpath = module or getattr(o.__class__, "__module__", None) or ""
# Core modules are always active
if any(searchpath.startswith(cm) for cm in settings.CORE_MODULES):
if searchpath and any(searchpath.startswith(cm) for cm in settings.CORE_MODULES):
return 'CORE'
if not app_cache:
_populate_app_cache()
while True:
app = None
while searchpath:
app = app_cache.get(searchpath)
if "." not in searchpath or app:
break
@@ -214,14 +216,27 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
responses.append((receiver, response))
return responses
def asend(self, sender: T, **named):
raise NotImplementedError() # NOQA
def asend_robust(self, sender: T, **named):
raise NotImplementedError() # NOQA
def _sorted_receivers(self, sender):
orig_list = self._live_receivers(sender)
orig_list, __ = self._live_receivers(sender)
def _receiver_module(receiver):
return getattr(receiver, "__module__", receiver.__class__.__module__)
def _receiver_name(receiver):
return getattr(receiver, "__name__", receiver.__class__.__name__)
sorted_list = sorted(
orig_list,
key=lambda receiver: (
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
receiver.__module__,
receiver.__name__,
0 if any(_receiver_module(receiver).startswith(m) for m in settings.CORE_MODULES) else 1,
_receiver_module(receiver),
_receiver_name(receiver),
)
)
return sorted_list

View File

@@ -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):

View File

@@ -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>

View File

@@ -41,9 +41,7 @@ from urllib.parse import quote, urljoin, urlparse
import webauthn
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import (
authenticate, login as auth_login, logout as auth_logout,
)
from django.contrib.auth import authenticate, logout as auth_logout
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import PermissionDenied
from django.db import transaction
@@ -67,7 +65,7 @@ from pretix.base.forms.auth import (
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
from pretix.helpers.http import get_client_ip, redirect_to_url
from pretix.helpers.security import handle_login_source
from pretix.helpers.security import handle_login_source, session_login
logger = logging.getLogger(__name__)
@@ -79,12 +77,12 @@ def process_login(request, user, keep_logged_in):
:return: This method returns a ``HttpResponse``.
"""
request.session['pretix_auth_long_session'] = settings.PRETIX_LONG_SESSIONS and keep_logged_in
next_url = get_auth_backends()[user.auth_backend].get_next_url(request)
if user.require_2fa:
logger.info(f"Backend login redirected to 2FA for user {user.pk}.")
request.session['pretix_auth_2fa_user'] = user.pk
request.session['pretix_auth_2fa_time'] = str(int(time.time()))
request.session['pretix_auth_long_session'] = settings.PRETIX_LONG_SESSIONS and keep_logged_in
twofa_url = reverse('control:auth.login.2fa')
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
twofa_url += '?next=' + quote(next_url)
@@ -93,10 +91,7 @@ def process_login(request, user, keep_logged_in):
logger.info(f"Backend login successful for user {user.pk}.")
pretix_successful_logins.inc(1)
handle_login_source(user, request)
auth_login(request, user)
t = int(time.time())
request.session['pretix_auth_login_time'] = t
request.session['pretix_auth_last_used'] = t
session_login(request, user, keep_logged_in)
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
return redirect_to_url(next_url)
return redirect('control:index')
@@ -221,11 +216,7 @@ def register(request):
)
user = authenticate(request=request, email=user.email, password=form.cleaned_data['password'])
user.log_action('pretix.control.auth.user.created', user=user)
auth_login(request, user)
request.session['pretix_auth_login_time'] = int(time.time())
request.session['pretix_auth_long_session'] = (
settings.PRETIX_LONG_SESSIONS and form.cleaned_data.get('keep_logged_in', False)
)
session_login(request, user, form.cleaned_data.get('keep_logged_in', False))
return redirect('control:index')
else:
form = RegistrationForm()
@@ -284,11 +275,7 @@ def invite(request, token):
)
user = authenticate(request=request, email=user.email, password=form.cleaned_data['password'])
user.log_action('pretix.control.auth.user.created', user=user)
auth_login(request, user)
request.session['pretix_auth_login_time'] = int(time.time())
request.session['pretix_auth_long_session'] = (
settings.PRETIX_LONG_SESSIONS and form.cleaned_data.get('keep_logged_in', False)
)
session_login(request, user, form.cleaned_data.get('keep_logged_in', False))
inv.team.members.add(request.user)
inv.team.log_action(
@@ -546,8 +533,7 @@ class Login2FAView(TemplateView):
logger.info(f"Backend login successful for user {self.user.pk} with 2FA.")
pretix_successful_logins.inc(1)
handle_login_source(self.user, request)
auth_login(request, self.user)
request.session['pretix_auth_login_time'] = int(time.time())
session_login(request, self.user, request.session["pretix_auth_long_session"])
del request.session['pretix_auth_2fa_user']
del request.session['pretix_auth_2fa_time']
if "next" in request.GET and url_has_allowed_host_and_scheme(request.GET.get("next"), allowed_hosts=None):

View File

@@ -80,6 +80,7 @@ from pretix.control.permissions import (
)
from pretix.control.views.auth import get_u2f_appid, get_webauthn_rp_id
from pretix.helpers.http import redirect_to_url
from pretix.helpers.security import session_reauth
from pretix.helpers.u2f import websafe_encode
REAL_DEVICE_TYPES = (TOTPDevice, WebAuthnDevice, U2FDevice)
@@ -159,9 +160,7 @@ class ReauthView(TemplateView):
valid = valid or self.form.is_valid()
if valid:
t = int(time.time())
request.session['pretix_auth_login_time'] = t
request.session['pretix_auth_last_used'] = t
session_reauth(request)
next_url = get_auth_backends()[request.user.auth_backend].get_next_url(request)
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
return redirect_to_url(next_url)
@@ -175,9 +174,7 @@ class ReauthView(TemplateView):
u = backend.request_authenticate(request)
if u and u == request.user:
next_url = backend.get_next_url(request)
t = int(time.time())
request.session['pretix_auth_login_time'] = t
request.session['pretix_auth_last_used'] = t
session_reauth(request)
if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None):
return redirect_to_url(next_url)
return redirect(reverse('control:index'))

View File

@@ -24,7 +24,8 @@ import logging
import time
from django.conf import settings
from django.contrib.gis.geoip2 import GeoIP2
from django.contrib.auth import login as auth_login
from django.contrib.gis import geoip2
from django.core.cache import cache
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -62,14 +63,20 @@ def get_user_agent_hash(request):
_geoip = None
def _get_country(request):
def get_geoip() -> geoip2.GeoIP2:
# See https://code.djangoproject.com/ticket/36988#ticket
global _geoip
if not _geoip:
_geoip = GeoIP2()
geoip2.SUPPORTED_DATABASE_TYPES.add("Geoacumen-Country")
if not _geoip:
_geoip = geoip2.GeoIP2()
return _geoip
def _get_country(request):
try:
res = _geoip.country(get_client_ip(request))
res = get_geoip().country(get_client_ip(request))
except AddressNotFoundError:
return None
return res['country_code']
@@ -174,3 +181,17 @@ def handle_login_source(user, request):
user=user,
locale=user.locale
)
def session_login(request, user, keep_logged_in=False):
auth_login(request, user)
session_reauth(request)
request.session["pretix_auth_long_session"] = (
settings.PRETIX_LONG_SESSIONS and keep_logged_in
)
def session_reauth(request):
t = int(time.time())
request.session['pretix_auth_login_time'] = t
request.session['pretix_auth_last_used'] = t

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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'),

View File

@@ -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 '

View File

@@ -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 %}

View File

@@ -49,7 +49,7 @@ from django.views.decorators.cache import cache_page
from django.views.decorators.gzip import gzip_page
from django.views.decorators.http import condition
from django.views.i18n import (
JavaScriptCatalog, get_formats, js_catalog_template,
JavaScriptCatalog, builtin_template_path, get_formats,
)
from lxml import html
@@ -170,7 +170,8 @@ def generate_widget_js(version, lang):
'September', 'October', 'November', 'December'
)
catalog = dict((k, v) for k, v in catalog.items() if k.startswith('widget\u0004') or k in str_wl)
template = Engine().from_string(js_catalog_template)
with builtin_template_path("i18n_catalog.js").open(encoding="utf-8") as fh:
template = Engine().from_string(fh.read())
context = Context({
'catalog_str': indent(json.dumps(
catalog, sort_keys=True, indent=2)) if catalog else None,

View File

@@ -534,6 +534,7 @@ X_FRAME_OPTIONS = 'DENY'
# URL settings
ROOT_URLCONF = 'pretix.multidomain.maindomain_urlconf'
FORMS_URLFIELD_ASSUME_HTTPS = True # transitional for django 6.0
WSGI_APPLICATION = 'pretix.wsgi.application'

View File

@@ -174,7 +174,7 @@ $(function () {
const fill_peppol_id = function () {
const vatId = dependents.vat_id.val();
if (vatId && vatId.startsWith("BE") && dependents.transmission_type.val() === "peppol") {
dependents.transmission_peppol_participant_id.val("0208:" + vatId.substring(2))
dependents.transmission_peppol_participant_id.val("0208:" + vatId.substring(2).replaceAll(".", ""))
}
}
dependents.vat_id.add(dependents.transmission_type).on("change", fill_peppol_id);

View File

@@ -23,9 +23,7 @@ filterwarnings =
error
ignore:.*invalid escape sequence.*:
ignore:The 'warn' method is deprecated:DeprecationWarning
ignore::django.utils.deprecation.RemovedInDjango51Warning:django.core.files.storage
ignore:.*index_together.*:django.utils.deprecation.RemovedInDjango51Warning:
ignore:.*get_storage_class.*:django.utils.deprecation.RemovedInDjango51Warning:compressor
ignore::django.utils.deprecation.RemovedInDjango60Warning:
ignore:.*This signal will soon be only available for plugins that declare to be organizer-level.*:DeprecationWarning:
ignore::DeprecationWarning:mt940
ignore::DeprecationWarning:cbor2

View File

@@ -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

View File

@@ -19,14 +19,13 @@
# 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/>.
#
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
import aiohttp
import pytest
import pytest_asyncio
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pytz import UTC
from pretix.base.models import (
Device, Event, Item, Organizer, Quota, SeatingPlan,
@@ -53,7 +52,7 @@ def organizer():
def event(organizer):
e = Event.objects.create(
organizer=organizer, name='Dummy', slug='dummy',
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC),
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=timezone.utc),
presale_end=now() + timedelta(days=300),
plugins='pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf',
is_public=True, live=True
@@ -109,8 +108,8 @@ def customer(event, membership_type):
def membership(event, membership_type, customer):
return customer.memberships.create(
membership_type=membership_type,
date_start=datetime(2017, 1, 1, 0, 0, tzinfo=UTC),
date_end=datetime(2099, 1, 1, 0, 0, tzinfo=UTC),
date_start=datetime(2017, 1, 1, 0, 0, tzinfo=timezone.utc),
date_end=datetime(2099, 1, 1, 0, 0, tzinfo=timezone.utc),
)