Compare commits

..

7 Commits

Author SHA1 Message Date
Raphael Michel
8fa715ac4b API: Allow to add debug_data to failed check-ins 2023-12-01 11:09:18 +01:00
Raphael Michel
6c479808d0 Fix crash PRETIXEU-9FC 2023-11-30 13:49:27 +01:00
Raphael Michel
bd14be485a Order change: Do not set invoice_dirty if invoicing is disabled 2023-11-30 11:51:41 +01:00
Raphael Michel
fbf362a91f Export management command: Fix bug in exporter detection 2023-11-30 11:49:16 +01:00
Raphael Michel
82704b60c7 Voucher form: Fix quota check for partially redeemed vouchers 2023-11-29 16:09:04 +01:00
Raphael Michel
b92feb382b Discounts: Fix scoping error with distinct subevents 2023-11-29 16:02:27 +01:00
Raphael Michel
66f934bba7 Bump version to 2023.11.0.dev0 2023-11-29 13:47:55 +01:00
15 changed files with 96 additions and 37 deletions

View File

@@ -59,7 +59,7 @@ dependencies = [
"dnspython==2.3.*",
"drf_ujson2==1.7.*",
"geoip2==4.*",
"importlib_metadata==7.*", # Polyfill, we can probably drop this once we require Python 3.10+
"importlib_metadata==6.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.3.*",

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2023.10.2"
__version__ = "2023.11.0.dev0"

View File

@@ -152,6 +152,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['POST'], url_name='failed_checkins')
@transaction.atomic()
def failed_checkins(self, *args, **kwargs):
additional_log_data = {}
if 'debug_data' in self.request.data:
# Intentionally undocumented, might be removed again
additional_log_data['debug_data'] = self.request.data.pop('debug_data')
serializer = FailedCheckinSerializer(
data=self.request.data,
context={'event': self.request.event}
@@ -194,14 +199,16 @@ class CheckinListViewSet(viewsets.ModelViewSet):
'reason_explanation': c.error_explanation,
'datetime': c.datetime,
'type': c.type,
'list': c.list.pk
'list': c.list.pk,
**additional_log_data,
}, user=self.request.user, auth=self.request.auth)
else:
self.request.event.log_action('pretix.event.checkin.unknown', data={
'datetime': c.datetime,
'type': c.type,
'list': c.list.pk,
'barcode': c.raw_barcode
'barcode': c.raw_barcode,
**additional_log_data,
}, user=self.request.user, auth=self.request.auth)
return Response(serializer.data, status=201)

View File

@@ -104,7 +104,7 @@ class Command(BaseCommand):
with language(locale), override(timezone):
for receiver, response in signal_result:
if not response:
return None
continue
ex = response(e, o, report_status)
if ex.identifier == options['export_provider']:
params = json.loads(options.get('parameters') or '{}')

View File

@@ -424,5 +424,10 @@ class Discount(LoggedModel):
break
for g in candidate_groups:
self._apply_min_count(positions, g, g, result)
self._apply_min_count(
positions,
[idx for idx in g if idx in condition_candidates],
[idx for idx in g if idx in benefit_candidates],
result
)
return result

View File

@@ -2620,7 +2620,7 @@ class OrderChangeManager:
i = self.order.invoices.filter(is_cancellation=False).last()
if self.reissue_invoice and self._invoice_dirty:
order_now_qualified = invoice_qualified(self.order)
invoice_should_be_generated = (
invoice_should_be_generated_now = (
self.event.settings.invoice_generate == "True" or (
self.event.settings.invoice_generate == "paid" and
self.open_payment is not None and
@@ -2635,13 +2635,16 @@ class OrderChangeManager:
not i.canceled
)
)
invoice_should_be_generated_later = not invoice_should_be_generated_now and (
self.event.settings.invoice_generate in ("True", "paid")
)
if order_now_qualified:
if invoice_should_be_generated:
if invoice_should_be_generated_now:
if i and not i.canceled:
self._invoices.append(generate_cancellation(i))
self._invoices.append(generate_invoice(self.order))
else:
elif invoice_should_be_generated_later:
self.order.invoice_dirty = True
self.order.save(update_fields=["invoice_dirty"])
else:

View File

@@ -219,17 +219,15 @@ class ExtValidationMixin:
def clean(self, *args, **kwargs):
data = super().clean(*args, **kwargs)
from ...base.models import CachedFile
if isinstance(data, (UploadedFile, CachedFile)):
filename = data.name if isinstance(data, UploadedFile) else data.filename
if isinstance(data, UploadedFile):
filename = data.name
ext = os.path.splitext(filename)[1]
ext = ext.lower()
if ext not in self.ext_whitelist:
raise forms.ValidationError(_("Filetype not allowed!"))
if ext in IMAGE_EXTS:
validate_uploaded_file_for_valid_image(data if isinstance(data, UploadedFile) else data.file)
validate_uploaded_file_for_valid_image(data)
return data
@@ -259,12 +257,6 @@ class CachedFileField(ExtFileField):
if isinstance(data, File):
if hasattr(data, '_uploaded_to'):
return data._uploaded_to
try:
self.clean(data)
except ValidationError:
return None
cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1),
date=now(),
@@ -276,9 +268,6 @@ class CachedFileField(ExtFileField):
cf.save()
data._uploaded_to = cf
return cf
if isinstance(data, CachedFile):
return data
return super().bound_data(data, initial)
def clean(self, *args, **kwargs):

View File

@@ -201,6 +201,8 @@ class VoucherForm(I18nModelForm):
cnt = len(data['codes']) * data.get('max_usages', 0)
else:
cnt = data.get('max_usages', 0)
if self.instance and self.instance.pk:
cnt -= self.instance.redeemed # these do not need quota any more
Voucher.clean_item_properties(
data, self.instance.event,

View File

@@ -267,7 +267,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
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],
min(free_seats, wle.availability[1])
min(free_seats, wle.availability[1]) if wle.availability[1] is not None else free_seats,
)
itemvar_cache[(wle.item, wle.variation, wle.subevent)] = wle.availability

View File

@@ -44,12 +44,11 @@ def validate_uploaded_file_for_valid_image(f):
# have to read the data into memory.
if hasattr(f, 'temporary_file_path'):
file = f.temporary_file_path()
elif hasattr(f, 'read'):
if hasattr(f, 'seek') and callable(f.seek):
f.seek(0)
file = BytesIO(f.read())
else:
file = BytesIO(f['content'])
if hasattr(f, 'read'):
file = BytesIO(f.read())
else:
file = BytesIO(f['content'])
try:
try:

View File

@@ -924,11 +924,6 @@ class StripePaymentIntentMethod(StripeMethod):
}
})
if self.method == "card":
params['statement_descriptor_suffix'] = self.statement_descriptor(payment)
else:
params['statement_descriptor'] = self.statement_descriptor(payment)
intent = stripe.PaymentIntent.create(
amount=self._get_amount(payment),
currency=self.event.currency.lower(),
@@ -940,6 +935,7 @@ class StripePaymentIntentMethod(StripeMethod):
event=self.event.slug.upper(),
code=payment.order.code
),
statement_descriptor=self.statement_descriptor(payment),
metadata={
'order': str(payment.order.id),
'event': self.event.id,

View File

@@ -35,7 +35,7 @@ from tests.const import SAMPLE_PNG
from pretix.api.serializers.item import QuestionSerializer
from pretix.base.models import (
Checkin, CheckinList, InvoiceAddress, Order, OrderPosition,
Checkin, CheckinList, InvoiceAddress, Order, OrderPosition, LogEntry,
)
@@ -1128,11 +1128,17 @@ def test_store_failed(token_client, organizer, clist, event, order):
), {
'raw_barcode': '123456',
'nonce': '4321',
'error_reason': 'invalid'
'error_reason': 'invalid',
'debug_data': {'foo': 'bar'},
}, format='json')
assert resp.status_code == 201
with scopes_disabled():
assert Checkin.all.filter(successful=False).exists()
for le in LogEntry.objects.filter():
print(le.parsed_data)
assert LogEntry.objects.filter(action_type='pretix.event.checkin.unknown').first().parsed_data['debug_data'] == {
'foo': 'bar'
}
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/failed_checkins/'.format(
organizer.slug, event.slug, clist.pk,
@@ -1162,7 +1168,7 @@ def test_store_failed(token_client, organizer, clist, event, order):
'raw_barcode': '123456',
'nonce': '1234',
'position': p.pk,
'error_reason': 'unpaid'
'error_reason': 'unpaid',
}, format='json')
assert resp.status_code == 201
with scopes_disabled():

View File

@@ -2006,6 +2006,20 @@ class OrderChangeManagerTests(TestCase):
).confirm()
assert self.order.invoices.count() == 3
@classscope(attr='o')
def test_reissue_invoice_paid_only_after_payment_only_if_enabled(self):
self.event.settings.invoice_generate = "False"
assert self.order.invoices.count() == 0
self.ocm.add_position(self.ticket, None, Decimal('2.00'))
self.ocm.commit()
assert self.order.invoices.count() == 0
self.order.refresh_from_db()
assert not self.order.invoice_dirty
self.order.payments.create(
provider='manual', amount=self.order.total
).confirm()
assert self.order.invoices.count() == 0
@classscope(attr='o')
def test_reissue_invoice_paid_stays_paid(self):
self.event.settings.invoice_generate = "paid"

View File

@@ -965,6 +965,31 @@ def test_limit_products(event, item, item2):
assert sorted(new_prices) == sorted(expected)
@pytest.mark.django_db
@scopes_disabled()
def test_limit_products_subevents_distinct(event, item, item2):
d1 = Discount(event=event, condition_min_count=2, benefit_discount_matching_percent=20, condition_all_products=False,
subevent_mode=Discount.SUBEVENT_MODE_DISTINCT)
d1.save()
d1.condition_limit_products.add(item)
positions = (
(item.pk, 1, Decimal('100.00'), False, False, Decimal('0.00')),
(item.pk, 2, Decimal('100.00'), False, False, Decimal('0.00')),
(item.pk, 3, Decimal('100.00'), False, False, Decimal('0.00')),
(item2.pk, 4, Decimal('50.00'), False, False, Decimal('0.00')),
)
expected = (
Decimal('80.00'),
Decimal('80.00'),
Decimal('80.00'),
Decimal('50.00'),
)
new_prices = [p for p, d in apply_discounts(event, 'web', positions)]
assert sorted(new_prices) == sorted(expected)
@pytest.mark.django_db
@scopes_disabled()
def test_sales_channels(event, item):

View File

@@ -365,6 +365,19 @@ class VoucherFormTest(SoupTestMixin, TransactionTestCase):
v.refresh_from_db()
assert v.valid_until < now()
def test_change_voucher_validity_to_valid_quota_full_already_redeemed(self):
self.quota_tickets.size = 1
self.quota_tickets.save()
with scopes_disabled():
v = self.event.vouchers.create(item=self.ticket, valid_until=now() - datetime.timedelta(days=3),
block_quota=True, redeemed=1, max_usages=2)
self._change_voucher(v, {
'valid_until_0': (now() + datetime.timedelta(days=3)).strftime('%Y-%m-%d'),
'valid_until_1': (now() + datetime.timedelta(days=3)).strftime('%H:%M:%S')
})
v.refresh_from_db()
assert v.valid_until > now()
def test_change_voucher_validity_to_valid_quota_free(self):
with scopes_disabled():
v = self.event.vouchers.create(item=self.ticket, valid_until=now() - datetime.timedelta(days=3),