mirror of
https://github.com/pretix/pretix.git
synced 2026-03-10 13:42:27 +00:00
Compare commits
7 Commits
pluggable-
...
voucher-cs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa2d202e07 | ||
|
|
5099fa16e0 | ||
|
|
f3fb1e66dc | ||
|
|
99e9690d48 | ||
|
|
e63e82e854 | ||
|
|
c662e627d5 | ||
|
|
f2121c7853 |
@@ -181,10 +181,11 @@ class WaitingListEntry(LoggedModel):
|
||||
block_quota=True,
|
||||
item_id=self.item_id,
|
||||
subevent_id=self.subevent_id,
|
||||
waitinglistentries__isnull=False
|
||||
waitinglistentries__isnull=False,
|
||||
seat__isnull=True
|
||||
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
|
||||
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
|
||||
if not free_seats:
|
||||
if free_seats < 1:
|
||||
raise WaitingListException(_('No seat with this product is currently available.'))
|
||||
|
||||
if '@' not in self.email:
|
||||
|
||||
@@ -423,7 +423,7 @@ def resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone) -> Tuple[Optio
|
||||
raise ValueError(f"Invalid timeframe '{frame}'")
|
||||
|
||||
|
||||
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
|
||||
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[datetime], Optional[datetime]]:
|
||||
"""
|
||||
Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of datetimes
|
||||
where the first element ist the first possible datetime within the timeframe and the second
|
||||
|
||||
@@ -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 "",
|
||||
|
||||
@@ -280,11 +280,12 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
|
||||
block_quota=True,
|
||||
item_id=wle.item_id,
|
||||
subevent=wle.subevent_id,
|
||||
waitinglistentries__isnull=False
|
||||
waitinglistentries__isnull=False,
|
||||
seat__isnull=True
|
||||
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
|
||||
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
|
||||
wle.availability = (
|
||||
Quota.AVAILABILITY_GONE if free_seats == 0 else wle.availability[0],
|
||||
Quota.AVAILABILITY_GONE if free_seats < 1 else wle.availability[0],
|
||||
min(free_seats, wle.availability[1]) if wle.availability[1] is not None else free_seats,
|
||||
)
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@ 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-02-19 22:00+0000\n"
|
||||
"PO-Revision-Date: 2026-03-05 20:00+0000\n"
|
||||
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/da/"
|
||||
">\n"
|
||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"da/>\n"
|
||||
"Language: da\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"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\n"
|
||||
"X-Generator: Weblate 5.16.1\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -34285,7 +34285,7 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/voucher.html:293
|
||||
#, python-format
|
||||
msgid "minimum amount to order: %(num)s"
|
||||
msgstr ""
|
||||
msgstr "Minimumsbestilling: %(num)s"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:76
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:160
|
||||
|
||||
@@ -5,8 +5,8 @@ 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-02-24 12:07+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2026-03-07 23:00+0000\n"
|
||||
"Last-Translator: argonimos <jonas@pfeiffer-wagner.de>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"de/>\n"
|
||||
"Language: de\n"
|
||||
@@ -14,7 +14,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\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
@@ -3845,7 +3845,7 @@ msgstr "Restbetrag"
|
||||
#, python-brace-format
|
||||
msgctxt "invoice"
|
||||
msgid "Invoice period: {daterange}"
|
||||
msgstr "Rechungsperiode: {daterange}"
|
||||
msgstr "Rechnungsperiode: {daterange}"
|
||||
|
||||
#: pretix/base/invoicing/pdf.py:1039
|
||||
msgctxt "invoice"
|
||||
|
||||
@@ -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-09 12:52+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ja/>\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 5.16.1\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -11448,7 +11448,7 @@ msgstr ""
|
||||
"{event}のご注文が完了しました。無料製品のみのご注文のため、\n"
|
||||
"お支払いは不要です。\n"
|
||||
"\n"
|
||||
"注文の詳細の変更やステータス確認は、以下のURLから行えます:\n"
|
||||
"注文の詳細の変更やステータス確認は、以下のURLから行えます\n"
|
||||
"{url}\n"
|
||||
"\n"
|
||||
"よろしくお願いいたします。\n"
|
||||
@@ -11750,7 +11750,7 @@ msgstr ""
|
||||
"{event}のご注文のお支払いを受け取りました。\n"
|
||||
"\n"
|
||||
"残念ながら、受け取った金額は必要な全額よりも少ないです。\n"
|
||||
"したがって、追加の**{pending_sum}**の支払いが不足しているため、\n"
|
||||
"したがって、追加の **{pending_sum}** の支払いが不足しているため、\n"
|
||||
"ご注文は未払いと見なされます。\n"
|
||||
"\n"
|
||||
"お支払い情報やご注文の状況は、以下のURLでご確認いただけます。\n"
|
||||
@@ -18428,7 +18428,7 @@ msgid ""
|
||||
"Do you really want to grant the application <strong>%(application)s</strong> "
|
||||
"access to your pretix account?"
|
||||
msgstr ""
|
||||
"本当にアプリケーション<strong>%(application)s</strong>にPretixアカウントへの"
|
||||
"本当にアプリケーション<strong>%(application)s</strong>にpretixアカウントへの"
|
||||
"アクセスを許可しますか?"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html:24
|
||||
@@ -24692,7 +24692,7 @@ msgstr "顧客履歴"
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer_anonymize.html:11
|
||||
#, python-format
|
||||
msgid "Anonymize customer #%(id)s"
|
||||
msgstr "顧客のID #%(id)s を匿名化"
|
||||
msgstr "顧客 #%(id)s を匿名化"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/customer_anonymize.html:16
|
||||
msgid "Are you sure you want to anonymize this customer account?"
|
||||
|
||||
@@ -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-04 16:57+0000\n"
|
||||
"PO-Revision-Date: 2026-03-09 12:52+0000\n"
|
||||
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
|
||||
"Language-Team: Dutch (Belgium) <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix/nl_BE/>\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"
|
||||
@@ -26981,6 +26981,10 @@ msgid ""
|
||||
"the affected data in your legislation, e.g. for reasons of taxation. In many "
|
||||
"countries, you need to keep some data in the live system in case of an audit."
|
||||
msgstr ""
|
||||
"Het is uw eigen verantwoordelijkheid om te controleren of u de gegevens "
|
||||
"volgens uw wetgeving mag verwijderen, bijvoorbeeld om fiscale redenen. In "
|
||||
"veel landen moet u bepaalde gegevens in het livesysteem bewaren voor het "
|
||||
"geval er een audit plaatsvindt."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/shredder/index.html:32
|
||||
msgid ""
|
||||
@@ -26988,81 +26992,87 @@ msgid ""
|
||||
"to store it offline. Some kinds of data (such as some payment information) "
|
||||
"as well as historical log data cannot be downloaded at the moment."
|
||||
msgstr ""
|
||||
"U kunt voor de meeste categorieën de gegevens gedeeltelijk downloaden om ze "
|
||||
"offline op te slaan. Sommige soorten gegevens (bijvoorbeeld sommige "
|
||||
"betalingsinformatie) en historische loggegevens kunnen momenteel niet worden "
|
||||
"gedownload."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/shredder/index.html:46
|
||||
msgid "Data selection"
|
||||
msgstr ""
|
||||
msgstr "Gegevensselectie"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/shredder/index.html:63
|
||||
msgid ""
|
||||
"We recommend not to remove this data because you might need it in case of a "
|
||||
"tax audit."
|
||||
msgstr ""
|
||||
"We raden aan om deze gegevens niet te verwijderen, omdat u ze mogelijk nodig "
|
||||
"hebt bij een belastingaudit."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:10
|
||||
msgctxt "subevent"
|
||||
msgid "Create multiple dates"
|
||||
msgstr ""
|
||||
msgstr "Meerdere datums aanmaken"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:35
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:146
|
||||
msgid "Repetition rule"
|
||||
msgstr ""
|
||||
msgstr "Regel voor herhaling"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:81
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:192
|
||||
#, python-format
|
||||
msgid "Repeat every %(interval)s %(freq)s, starting at %(start)s."
|
||||
msgstr ""
|
||||
msgstr "Herhaal ieder(e) %(interval)s %(freq)s, beginnend op %(start)s."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:258
|
||||
msgctxt "subevent"
|
||||
msgid "Preview"
|
||||
msgstr ""
|
||||
msgstr "Voorbeeldweergave"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:265
|
||||
msgctxt "subevent"
|
||||
msgid "Times"
|
||||
msgstr ""
|
||||
msgstr "Tijden"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:339
|
||||
msgid "Start of first slot"
|
||||
msgstr ""
|
||||
msgstr "Begin van eerste tijdsslot"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:345
|
||||
msgid "End of time slots"
|
||||
msgstr ""
|
||||
msgstr "Einde van tijdsslots"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:351
|
||||
msgid "Length of slots"
|
||||
msgstr ""
|
||||
msgstr "Lengte van tijdsslots"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:360
|
||||
msgid "Break between slots"
|
||||
msgstr ""
|
||||
msgstr "Pauze tussen tijdsslots"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:370
|
||||
msgid "Create"
|
||||
msgstr ""
|
||||
msgstr "Aanmaken"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:377
|
||||
msgid "Add a single time slot"
|
||||
msgstr ""
|
||||
msgstr "Eén tijdsslot toevoegen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:379
|
||||
msgid "Add many time slots"
|
||||
msgstr ""
|
||||
msgstr "Meerdere tijdsslots toevoegen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:481
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:266
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:124
|
||||
msgid "Add a new quota"
|
||||
msgstr ""
|
||||
msgstr "Nieuw quotum toevoegen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:485
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:128
|
||||
msgid "Product settings"
|
||||
msgstr ""
|
||||
msgstr "Productinstellingen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:487
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:130
|
||||
@@ -27070,6 +27080,8 @@ msgid ""
|
||||
"These settings are optional, if you leave them empty, the default values "
|
||||
"from the product settings will be used."
|
||||
msgstr ""
|
||||
"Deze instellingen zijn optioneel. Als u deze instellingen leeg laat, zullen "
|
||||
"de standaardwaarden uit de productinstellingen worden gebruikt."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:523
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:166
|
||||
|
||||
@@ -29,6 +29,8 @@ from pretix.base.models import (
|
||||
Event, Item, ItemVariation, Organizer, Quota, Team, User, Voucher,
|
||||
WaitingListEntry,
|
||||
)
|
||||
from pretix.base.models.seating import Seat, SeatingPlan
|
||||
from pretix.base.models.waitinglist import WaitingListException
|
||||
from pretix.control.views.dashboards import waitinglist_widgets
|
||||
|
||||
|
||||
@@ -55,11 +57,11 @@ def env():
|
||||
WaitingListEntry.objects.create(
|
||||
event=event, item=item1, email='success@example.org', voucher=v
|
||||
)
|
||||
v = Voucher.objects.create(item=item1, event=event, block_quota=True, redeemed=0, valid_until=now() - timedelta(days=5))
|
||||
v = Voucher.objects.create(item=item2, event=event, block_quota=True, redeemed=0, valid_until=now() - timedelta(days=5))
|
||||
WaitingListEntry.objects.create(
|
||||
event=event, item=item2, email='expired@example.org', voucher=v
|
||||
)
|
||||
v = Voucher.objects.create(item=item1, event=event, block_quota=True, redeemed=0, valid_until=now() + timedelta(days=5))
|
||||
v = Voucher.objects.create(item=item2, event=event, block_quota=True, redeemed=0, valid_until=now() + timedelta(days=5))
|
||||
WaitingListEntry.objects.create(
|
||||
event=event, item=item2, email='valid@example.org', voucher=v
|
||||
)
|
||||
@@ -345,5 +347,75 @@ def test_dashboard(client, env):
|
||||
quota.items.add(env['item1'])
|
||||
w = waitinglist_widgets(env['event'])
|
||||
|
||||
assert '1' in w[0]['content']
|
||||
assert '2' in w[0]['content']
|
||||
assert '5' in w[1]['content']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_waitinglist_seat_calc(client, env):
|
||||
item = env['item1']
|
||||
event = env['event']
|
||||
wle = env['wle']
|
||||
|
||||
SeatingPlan.objects.create(
|
||||
name="Plan", organizer=event.organizer, layout="{}"
|
||||
)
|
||||
event.seat_category_mappings.create(
|
||||
layout_category='Stalls', product=item
|
||||
)
|
||||
for i in range(2):
|
||||
event.seats.create(seat_number=f"A{i}", product=item, seat_guid=f"A{i}")
|
||||
|
||||
quota = Quota.objects.create(event=event, size=10)
|
||||
quota.items.add(item)
|
||||
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
|
||||
# Calculated availability should not be more than number of available seats
|
||||
response = client.get('/control/event/dummy/dummy/waitinglist/')
|
||||
assert len(response.context['entries']) == 5
|
||||
for entry in response.context['entries']:
|
||||
assert entry.availability == (Quota.AVAILABILITY_OK, 2)
|
||||
|
||||
# Sending out a voucher reduces availability by 1
|
||||
with scopes_disabled():
|
||||
wle.send_voucher()
|
||||
|
||||
voucher = wle.voucher
|
||||
assert voucher
|
||||
|
||||
response = client.get('/control/event/dummy/dummy/waitinglist/')
|
||||
assert len(response.context['entries']) == 4
|
||||
for entry in response.context['entries']:
|
||||
assert entry.availability == (Quota.AVAILABILITY_OK, 1)
|
||||
|
||||
# Assigning a seat to a voucher does not decrease availability further
|
||||
with scopes_disabled():
|
||||
voucher.seat = Seat.objects.get(seat_guid="A0")
|
||||
voucher.save()
|
||||
|
||||
response = client.get('/control/event/dummy/dummy/waitinglist/')
|
||||
assert len(response.context['entries']) == 4
|
||||
for entry in response.context['entries']:
|
||||
assert entry.availability == (Quota.AVAILABILITY_OK, 1)
|
||||
|
||||
with scopes_disabled():
|
||||
wle2 = WaitingListEntry.objects.filter(item=item, voucher__isnull=True).first()
|
||||
wle2.send_voucher()
|
||||
|
||||
# Overbooking is handled correctly
|
||||
# Regression test for calculation that used `not free_seats` instead of `free_seats < 1`
|
||||
with scopes_disabled():
|
||||
# Block seat
|
||||
seat = Seat.objects.get(seat_guid="A1")
|
||||
seat.blocked = True
|
||||
seat.save()
|
||||
|
||||
response = client.get('/control/event/dummy/dummy/waitinglist/')
|
||||
assert len(response.context['entries']) == 3
|
||||
for entry in response.context['entries']:
|
||||
assert entry.availability == (Quota.AVAILABILITY_GONE, -1)
|
||||
|
||||
with scopes_disabled(), pytest.raises(WaitingListException):
|
||||
wle3 = WaitingListEntry.objects.filter(item=item, voucher__isnull=True).first()
|
||||
wle3.send_voucher()
|
||||
|
||||
Reference in New Issue
Block a user