Compare commits

..

3 Commits

Author SHA1 Message Date
Richard Schreiber
e54837c532 remove print statement 2023-08-23 09:51:12 +02:00
Raphael Michel
bc49f0f7f1 Fix cache invalidation 2023-08-23 09:47:05 +02:00
Raphael Michel
3e122e0270 Fix quota cache mixup 2023-08-22 13:00:16 +02:00
35 changed files with 203 additions and 629 deletions

View File

@@ -23,14 +23,10 @@ limit_products list of integers List of product
restrict_to_status list List of order states to restrict recipients to. Valid
entries are ``p`` for paid, ``e`` for expired, ``c`` for canceled,
``n__pending_approval`` for pending approval,
``n__not_pending_approval_and_not_valid_if_pending`` for payment
pending, ``n__valid_if_pending`` for payment pending but already confirmed,
``n__not_pending_approval_and_not_valid_if_pending`` for payment pending,
``n__valid_if_pending`` for payment pending but already confirmed,
and ``n__pending_overdue`` for pending with payment overdue.
The default is ``["p", "n__valid_if_pending"]``.
checked_in_status string Check-in status to restrict recipients to. Valid strings are:
``null`` for no filtering (default), ``checked_in`` for
limiting to attendees that are or have been checked in, and
``no_checkin`` for limiting to attendees who have not checked in.
date_is_absolute boolean If ``true``, the email is set at a specific point in time.
send_date datetime If ``date_is_absolute`` is set: Date and time to send the email.
send_offset_days integer If ``date_is_absolute`` is not set, this is the number of days
@@ -93,7 +89,6 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": null,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -144,7 +139,6 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": null,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -186,7 +180,6 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": "checked_in",
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -216,7 +209,6 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": "checked_in",
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
@@ -274,7 +266,6 @@ Endpoints
"n__not_pending_approval_and_not_valid_if_pending",
"n__valid_if_pending"
],
"checked_in_status": "checked_in",
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",

View File

@@ -415,7 +415,6 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
'subeventitem_set',
'subeventitemvariation_set',
'meta_values',
'meta_values__property',
Prefetch(
'seat_category_mappings',
to_attr='_seat_category_mappings',

View File

@@ -62,27 +62,27 @@ class NamespacedCache:
prefix = int(time.time())
self.cache.set(self.prefixkey, prefix)
def set(self, key: str, value: any, timeout: int=300):
def set(self, key: str, value: str, timeout: int=300):
return self.cache.set(self._prefix_key(key), value, timeout)
def get(self, key: str) -> any:
def get(self, key: str) -> str:
return self.cache.get(self._prefix_key(key, known_prefix=self._last_prefix))
def get_or_set(self, key: str, default: Callable, timeout=300) -> any:
def get_or_set(self, key: str, default: Callable, timeout=300) -> str:
return self.cache.get_or_set(
self._prefix_key(key, known_prefix=self._last_prefix),
default=default,
timeout=timeout
)
def get_many(self, keys: List[str]) -> Dict[str, any]:
def get_many(self, keys: List[str]) -> Dict[str, str]:
values = self.cache.get_many([self._prefix_key(key) for key in keys])
newvalues = {}
for k, v in values.items():
newvalues[self._strip_prefix(k)] = v
return newvalues
def set_many(self, values: Dict[str, any], timeout=300):
def set_many(self, values: Dict[str, str], timeout=300):
newvalues = {}
for k, v in values.items():
newvalues[self._prefix_key(k)] = v

View File

@@ -549,9 +549,7 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('End date'))
headers += [
_('Product'),
_('Product ID'),
_('Variation'),
_('Variation ID'),
_('Price'),
_('Tax rate'),
_('Tax rule'),
@@ -658,9 +656,7 @@ class OrderListExporter(MultiSheetListExporter):
row.append('')
row += [
str(op.item),
str(op.item_id),
str(op.variation) if op.variation else '',
str(op.variation_id) if op.variation_id else '',
op.price,
op.tax_rate,
str(op.tax_rule) if op.tax_rule else '',

View File

@@ -340,17 +340,10 @@ class TaxRule(LoggedModel):
rules = self._custom_rules
if invoice_address:
for r in rules:
if r['country'] == 'ZZ': # Rule: Any country
pass
elif r['country'] == 'EU': # Rule: Any EU country
if not is_eu_country(invoice_address.country):
continue
elif '-' in r['country']: # Rule: Specific country and state
if r['country'] != str(invoice_address.country) + '-' + str(invoice_address.state):
continue
else: # Rule: Specific country
if r['country'] != str(invoice_address.country):
continue
if r['country'] == 'EU' and not is_eu_country(invoice_address.country):
continue
if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country):
continue
if r['address_type'] == 'individual' and invoice_address.is_business:
continue
if r['address_type'] in ('business', 'business_vat_id') and not invoice_address.is_business:

View File

@@ -336,12 +336,6 @@ class BasePaymentProvider:
help_text=_('Users will not be able to choose this payment provider after the given date.'),
required=False,
)),
('_availability_start',
RelativeDateField(
label=_('Available from'),
help_text=_('Users will not be able to choose this payment provider before the given date.'),
required=False,
)),
('_total_min',
forms.DecimalField(
label=_('Minimum order total'),
@@ -545,57 +539,40 @@ class BasePaymentProvider:
return form
def _convert_availability_date_to_absolute(self, rel_date, cart_id=None, order=None):
if not rel_date:
return None
# In an event series, we use min() here, which makes it less restrictive than max() and thus makes
# it harder to put one self into a situation where no payment provider is available.
if self.event.has_subevents and cart_id:
dates = [
rel_date.datetime(se).date()
for se in self.event.subevents.filter(
id__in=CartPosition.objects.filter(
cart_id=cart_id, event=self.event
).values_list('subevent', flat=True)
)
]
return min(dates) if dates else None
elif self.event.has_subevents and order:
dates = [
rel_date.datetime(se).date()
for se in self.event.subevents.filter(
id__in=order.positions.values_list('subevent', flat=True)
)
]
return min(dates) if dates else None
elif self.event.has_subevents:
raise NotImplementedError('Payment provider is not subevent-ready.')
else:
return rel_date.datetime(self.event).date()
def _is_available_by_time(self, now_dt=None, cart_id=None, order=None):
def _is_still_available(self, now_dt=None, cart_id=None, order=None):
now_dt = now_dt or now()
tz = ZoneInfo(self.event.settings.timezone)
try:
availability_start = self._convert_availability_date_to_absolute(
self.settings.get('_availability_start', as_type=RelativeDateWrapper), cart_id, order)
availability_date = self.settings.get('_availability_date', as_type=RelativeDateWrapper)
if availability_date:
if self.event.has_subevents and cart_id:
dates = [
availability_date.datetime(se).date()
for se in self.event.subevents.filter(
id__in=CartPosition.objects.filter(
cart_id=cart_id, event=self.event
).values_list('subevent', flat=True)
)
]
availability_date = min(dates) if dates else None
elif self.event.has_subevents and order:
dates = [
availability_date.datetime(se).date()
for se in self.event.subevents.filter(
id__in=order.positions.values_list('subevent', flat=True)
)
]
availability_date = min(dates) if dates else None
elif self.event.has_subevents:
logger.error('Payment provider is not subevent-ready.')
return False
else:
availability_date = availability_date.datetime(self.event).date()
if availability_start:
if availability_start > now_dt.astimezone(tz).date():
return False
if availability_date:
return availability_date >= now_dt.astimezone(tz).date()
availability_end = self._convert_availability_date_to_absolute(
self.settings.get('_availability_date', as_type=RelativeDateWrapper), cart_id, order)
if availability_end:
if availability_end < now_dt.astimezone(tz).date():
return False
return True
except NotImplementedError:
logger.exception('Unable to check availability')
return False
return True
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
"""
@@ -604,9 +581,9 @@ class BasePaymentProvider:
user will not be able to select this payment method. This will only be called
during checkout, not on retrying.
The default implementation checks for the ``_availability_date`` setting to be either unset or in the future
and for the ``_availability_from``, ``_total_max``, and ``_total_min`` requirements to be met. It also checks
the ``_restrict_countries`` and ``_restrict_to_sales_channels`` setting.
The default implementation checks for the _availability_date setting to be either unset or in the future
and for the _total_max and _total_min requirements to be met. It also checks the ``_restrict_countries``
and ``_restrict_to_sales_channels`` setting.
:param total: The total value without the payment method fee, after taxes.
@@ -615,7 +592,7 @@ class BasePaymentProvider:
The ``total`` parameter has been added. For backwards compatibility, this method is called again
without this parameter if it raises a ``TypeError`` on first try.
"""
timing = self._is_available_by_time(cart_id=get_or_create_cart_id(request))
timing = self._is_still_available(cart_id=get_or_create_cart_id(request))
pricing = True
if (self.settings._total_max is not None or self.settings._total_min is not None) and total is None:
@@ -799,8 +776,8 @@ class BasePaymentProvider:
Will be called to check whether it is allowed to change the payment method of
an order to this one.
The default implementation checks for the ``_availability_date`` setting to be either unset or in the future,
as well as for the ``_availabilty_from``, ``_total_max``, ``_total_min``, and ``_restricted_countries`` settings.
The default implementation checks for the _availability_date setting to be either unset or in the future,
as well as for the _total_max, _total_min and _restricted_countries settings.
:param order: The order object
"""
@@ -827,7 +804,7 @@ class BasePaymentProvider:
if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
return False
return self._is_available_by_time(order=order)
return self._is_still_available(order=order)
def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str]:
"""

View File

@@ -702,10 +702,10 @@ def get_seat(op: OrderPosition):
def generate_compressed_addon_list(op, order, event):
itemcount = defaultdict(int)
addons = [p for p in (
addons = (
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
else op.addons.select_related('item', 'variation')
) if not p.canceled]
)
for pos in addons:
itemcount[pos.item, pos.variation] += 1

View File

@@ -1078,7 +1078,6 @@ class CartManager:
quotas_ok = _get_quota_availability(self._quota_diff, self.now_dt)
err = None
new_cart_positions = []
deleted_positions = set()
err = err or self._check_min_max_per_product()
@@ -1090,10 +1089,7 @@ class CartManager:
if op.position.expires > self.now_dt:
for q in op.position.quotas:
quotas_ok[q] += 1
addons = op.position.addons.all()
deleted_positions |= {a.pk for a in addons}
addons.delete()
deleted_positions.add(op.position.pk)
op.position.addons.all().delete()
op.position.delete()
elif isinstance(op, (self.AddOperation, self.ExtendOperation)):
@@ -1243,28 +1239,20 @@ class CartManager:
if op.seat and not op.seat.is_available(ignore_cart=op.position, sales_channel=self._sales_channel,
ignore_voucher_id=op.position.voucher_id):
err = err or error_messages['seat_unavailable']
addons = op.position.addons.all()
deleted_positions |= {a.pk for a in addons}
deleted_positions.add(op.position.pk)
addons.delete()
op.position.addons.all().delete()
op.position.delete()
elif available_count == 1:
op.position.expires = self._expiry
op.position.listed_price = op.listed_price
op.position.price_after_voucher = op.price_after_voucher
# op.position.price will be updated by recompute_final_prices_and_taxes()
if op.position.pk not in deleted_positions:
try:
op.position.save(force_update=True, update_fields=['expires', 'listed_price', 'price_after_voucher'])
except DatabaseError:
# Best effort... The position might have been deleted in the meantime!
pass
try:
op.position.save(force_update=True, update_fields=['expires', 'listed_price', 'price_after_voucher'])
except DatabaseError:
# Best effort... The position might have been deleted in the meantime!
pass
elif available_count == 0:
addons = op.position.addons.all()
deleted_positions |= {a.pk for a in addons}
deleted_positions.add(op.position.pk)
addons.delete()
op.position.addons.all().delete()
op.position.delete()
else:
raise AssertionError("ExtendOperation cannot affect more than one item")

View File

@@ -22,15 +22,13 @@
import sys
from datetime import timedelta
from django.db.models import (
Exists, F, OuterRef, Prefetch, Q, Sum, prefetch_related_objects,
)
from django.db.models import Exists, F, OuterRef, Q, Sum
from django.dispatch import receiver
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import (
Event, EventMetaValue, SeatCategoryMapping, User, WaitingListEntry,
Event, SeatCategoryMapping, User, WaitingListEntry,
)
from pretix.base.models.waitinglist import WaitingListException
from pretix.base.services.tasks import EventTask
@@ -61,21 +59,8 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
seats_available[(m.product_id, m.subevent_id)] = num_free_seets_for_product - num_valid_vouchers_for_product
prefetch_related_objects(
[event.organizer],
'meta_properties'
)
prefetch_related_objects(
[event],
Prefetch(
'meta_values',
EventMetaValue.objects.select_related('property'),
to_attr='meta_values_cached'
)
)
qs = event.waitinglistentries.filter(
voucher__isnull=True
qs = WaitingListEntry.objects.filter(
event=event, voucher__isnull=True
).select_related('item', 'variation', 'subevent').prefetch_related(
'item__quotas', 'variation__quotas'
).order_by('-priority', 'created')

View File

@@ -210,8 +210,6 @@ def slow_delete(qs, batch_size=1000, sleep_time=.5, progress_callback=None, prog
break
if total_deleted >= 0.8 * batch_size:
time.sleep(sleep_time)
if progress_callback and progress_total:
progress_callback((progress_offset + total_deleted) / progress_total)
return total_deleted

View File

@@ -38,7 +38,6 @@ from decimal import Decimal
from urllib.parse import urlencode, urlparse
from zoneinfo import ZoneInfo
import pycountry
from django import forms
from django.conf import settings
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
@@ -66,8 +65,7 @@ from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
PERSON_NAME_TITLE_GROUPS, validate_event_settings,
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings,
)
from pretix.base.validators import multimail_validate
from pretix.control.forms import (
@@ -1430,20 +1428,9 @@ class CountriesAndEU(CachedCountries):
cache_subkey = 'with_any_or_eu'
class CountriesAndEUAndStates(CountriesAndEU):
def __iter__(self):
for country_code, country_name in super().__iter__():
yield country_code, country_name
if country_code in COUNTRIES_WITH_STATE_IN_ADDRESS:
types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[country_code]
yield from sorted(((state.code, country_name + " - " + state.name)
for state in pycountry.subdivisions.get(country_code=country_code)
if state.type in types), key=lambda s: s[1])
class TaxRuleLineForm(I18nForm):
country = LazyTypedChoiceField(
choices=CountriesAndEUAndStates(),
choices=CountriesAndEU(),
required=False
)
address_type = forms.ChoiceField(

View File

@@ -340,9 +340,6 @@ class VoucherBulkForm(VoucherForm):
def clean_send_recipients(self):
raw = self.cleaned_data['send_recipients']
if self.cleaned_data.get('send', None) is False:
# No need to validate addresses if the section was turned off
return []
if not raw:
return []
r = raw.split('\n')

View File

@@ -198,12 +198,12 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
else item.check_quotas(subevent=subevent, count_waitinglist=False, _cache=quota_cache)
)
if row[1] is None:
happy += wlt['cnt']
happy += 1
elif row[1] > 0:
happy += min(wlt['cnt'], row[1])
happy += 1
for q in quotas:
if q.size is not None:
quota_cache[q.pk] = (quota_cache[q.pk][0], quota_cache[q.pk][1] - min(wlt['cnt'], row[1]))
quota_cache[q.pk] = (quota_cache[q.pk][0], quota_cache[q.pk][1] - 1)
widgets.append({
'content': None if lazy else NUM_WIDGET.format(

View File

@@ -1054,8 +1054,8 @@ class DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, Organizer
limit_events_list=Subquery(
Device.limit_events.through.objects.filter(
device_id=OuterRef('pk')
).order_by().values('device_id').annotate(
g=GroupConcat('event_id', separator=',', ordered=True)
).order_by('device_id', 'event_id').values('device_id').annotate(
g=GroupConcat('event_id', separator=',')
).values('g')
)
)

View File

@@ -66,26 +66,18 @@ class GroupConcat(Aggregate):
function = 'group_concat'
template = '%(function)s(%(field)s, "%(separator)s")'
def __init__(self, *expressions, ordered=False, **extra):
self.ordered = ordered
def __init__(self, *expressions, **extra):
if 'separator' not in extra:
# For PostgreSQL separator is an obligatory
extra.update({'separator': ','})
super().__init__(*expressions, **extra)
def as_postgresql(self, compiler, connection):
if self.ordered:
return super().as_sql(
compiler, connection,
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s' ORDER BY %(field)s ASC)",
)
else:
return super().as_sql(
compiler, connection,
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s')",
)
return super().as_sql(
compiler, connection,
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s')",
)
class ReplicaRouter:

View File

@@ -7,16 +7,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-27 11:49+0000\n"
"PO-Revision-Date: 2023-08-24 04:00+0000\n"
"Last-Translator: Alain <alain@waag.org>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
"\n"
"PO-Revision-Date: 2023-07-16 22:00+0000\n"
"Last-Translator: Freek Engelbarts <freekengelbarts@gmail.com>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/"
">\n"
"Language: nl\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 4.18.2\n"
"X-Generator: Weblate 4.17\n"
#: pretix/_base_settings.py:78
msgid "English"
@@ -534,8 +534,10 @@ msgid "Waiting list entry deleted"
msgstr "Wachtlijstitem verwijderd"
#: pretix/api/webhooks.py:351
#, fuzzy
#| msgid "Waiting list entries"
msgid "Waiting list entry received voucher"
msgstr "Wachtlijstitem heeft voucher ontvangen"
msgstr "Wachtlijstitems"
#: pretix/base/addressvalidation.py:100 pretix/base/addressvalidation.py:103
#: pretix/base/addressvalidation.py:108 pretix/base/forms/questions.py:938
@@ -551,11 +553,11 @@ msgstr "Dit veld is verplicht."
#: pretix/base/addressvalidation.py:213
msgid "Enter a postal code in the format XXX."
msgstr "Postcode in het formaat XXX invoeren."
msgstr "Voer een postcode in in het formaat XXX."
#: pretix/base/addressvalidation.py:222 pretix/base/addressvalidation.py:224
msgid "Enter a postal code in the format XXXX."
msgstr "Postcode in het format XXXX invoeren."
msgstr "Voer een postcode in in het format XXXX."
#: pretix/base/auth.py:143
#, python-brace-format
@@ -2309,7 +2311,7 @@ msgstr ""
#: pretix/base/exporters/orderlist.py:887
msgid "Converted from legacy version"
msgstr "Vanuit oudere versie geconverteerd"
msgstr ""
#: pretix/base/exporters/orderlist.py:949
msgid "Payments and refunds"
@@ -4378,7 +4380,7 @@ msgstr ""
#: pretix/base/models/items.py:662
msgid "Reusable media type"
msgstr "Mediatype"
msgstr ""
#: pretix/base/models/items.py:664
msgid ""
@@ -6144,7 +6146,7 @@ msgstr "Vul een geldige taalcode in."
#: pretix/base/orderimport.py:669 pretix/base/orderimport.py:692
#, python-brace-format
msgid "Could not parse {value} as a date and time."
msgstr "Kon {value} niet als datum en tijd herkennen."
msgstr ""
#: pretix/base/orderimport.py:711
msgid "Please enter a valid sales channel."
@@ -6845,7 +6847,7 @@ msgstr "Geldig tot"
#: pretix/base/pdf.py:457
msgid "Reusable Medium ID"
msgstr "Media-ID"
msgstr ""
#: pretix/base/pdf.py:462
msgid "Seat: Full name"
@@ -7522,8 +7524,6 @@ msgstr "Geen toestemming"
#: pretix/base/services/export.py:221
msgid "Your exported data exceeded the size limit for scheduled exports."
msgstr ""
"De door u geëxporteerde data overschrijdt de grootte-limiet voor geplande "
"exports."
#: pretix/base/services/invoices.py:103
#, python-brace-format
@@ -7766,7 +7766,10 @@ msgid "Your cart is empty."
msgstr "Uw winkelwagen is leeg."
#: pretix/base/services/orders.py:138
#, python-format
#, fuzzy, python-format
#| msgid ""
#| "You cannot select more than %(max)s items of the product %(product)s. We "
#| "removed the surplus items from your cart."
msgid ""
"You cannot select more than %(max)s item of the product %(product)s. We "
"removed the surplus items from your cart."
@@ -7774,11 +7777,11 @@ msgid_plural ""
"You cannot select more than %(max)s items of the product %(product)s. We "
"removed the surplus items from your cart."
msgstr[0] ""
"U kunt van het product %(product)s niet meer dan %(max)s per bestelling "
"kiezen. We hebben de overtallige producten uit uw winkelwagen verwijderd."
"U kunt niet meer dan %(max)s kopieën van het product %(product)s kiezen. We "
"hebben het overschot uit uw winkelwagen verwijderd."
msgstr[1] ""
"U kunt van het product %(product)s niet meer dan %(max)s per bestelling "
"kiezen. We hebben de overtallige producten uit uw winkelwagen verwijderd."
"U kunt niet meer dan %(max)s kopieën van het product %(product)s kiezen. We "
"hebben het overschot uit uw winkelwagen verwijderd."
#: pretix/base/services/orders.py:147
msgid "The booking period has ended."
@@ -7828,9 +7831,10 @@ msgstr ""
"niet geldig voor dit item. We hebben dit item uit uw winkelwagen verwijderd."
#: pretix/base/services/orders.py:168
#, fuzzy
#| msgid "You need a valid voucher code to order this product."
msgid "You need a valid voucher code to order one of the products."
msgstr ""
"U heeft een geldige vouchercode nodig om een van de producten te bestellen."
msgstr "U heeft een geldige vouchercode nodig om dit product te bestellen."
#: pretix/base/services/orders.py:170
msgid ""
@@ -7869,8 +7873,10 @@ msgstr ""
"is besteld."
#: pretix/base/services/orders.py:210
#, fuzzy
#| msgid "The order has been canceled."
msgid "The order was not canceled."
msgstr "De bestelling is niet geannuleerd."
msgstr "De bestelling is geannuleerd."
#: pretix/base/services/orders.py:265 pretix/control/forms/orders.py:120
msgid "The new expiry date needs to be in the future."
@@ -7906,8 +7912,10 @@ msgstr ""
"bestelling is betaald."
#: pretix/base/services/orders.py:918
#, fuzzy
#| msgid "This payment method does not support automatic refunds."
msgid "The selected payment methods do not cover the total balance."
msgstr "Deze betalingsmethode dekt het volledige bedrag niet."
msgstr "Deze betalingsmethode ondersteunt geen automatische terugbetalingen."
#: pretix/base/services/orders.py:990
msgid ""
@@ -8062,8 +8070,10 @@ msgid "Something happened in your event after the export, please try again."
msgstr "Er is iets gebeurd in uw evenement na de export, probeer het opnieuw."
#: pretix/base/services/shredder.py:177
#, fuzzy
#| msgid "Payment completed."
msgid "Data shredding completed"
msgstr "Verwijderen van data voltooid."
msgstr "Betaling voltooid."
#: pretix/base/services/stats.py:210
msgid "Uncategorized"
@@ -10090,7 +10100,19 @@ msgid "Your order is pending payment: {code}"
msgstr "Uw bestelling wacht op betaling: {code}"
#: pretix/base/settings.py:2316
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid ""
#| "Hello,\n"
#| "\n"
#| "we did not yet receive a full payment for your order for {event}.\n"
#| "Please keep in mind that we only guarantee your order if we receive\n"
#| "your payment before {expire_date}.\n"
#| "\n"
#| "You can view the payment information and the status of your order at\n"
#| "{url}\n"
#| "\n"
#| "Best regards, \n"
#| "Your {event} team"
msgid ""
"Hello,\n"
"\n"
@@ -10113,7 +10135,7 @@ msgstr ""
"U kunt de betalingsinformatie en de status van uw bestelling inzien op\n"
"{url}.\n"
"\n"
"Met vriendelijke groet, \n"
"Met vriendelijke groet,\n"
"De organisatoren van {event}"
#: pretix/base/settings.py:2329
@@ -10123,7 +10145,19 @@ msgid "Incomplete payment received: {code}"
msgstr "Betaling ontvangen voor uw bestelling: {code}"
#: pretix/base/settings.py:2333
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid ""
#| "Hello,\n"
#| "\n"
#| "we did not yet receive a full payment for your order for {event}.\n"
#| "Please keep in mind that we only guarantee your order if we receive\n"
#| "your payment before {expire_date}.\n"
#| "\n"
#| "You can view the payment information and the status of your order at\n"
#| "{url}\n"
#| "\n"
#| "Best regards, \n"
#| "Your {event} team"
msgid ""
"Hello,\n"
"\n"
@@ -10141,11 +10175,10 @@ msgid ""
msgstr ""
"Hallo,\n"
"\n"
"We hebben een betaling ontvangen voor {event}\n"
"\n"
"Helaas is het ontvangen bedrag minder dan het volledige verschuldigde "
"bedrag. Graag nog het bedrag van **{pending_sum}** voldoen om de bestelling "
"te voltooien.\n"
"We hebben nog geen volledige betaling ontvangen voor uw bestelling voor "
"{event}.\n"
"We kunnen uw bestelling alleen garanderen als we uw betaling ontvangen\n"
"voor {expire_date}.\n"
"\n"
"U kunt de betalingsinformatie en de status van uw bestelling inzien op\n"
"{url}.\n"
@@ -10334,7 +10367,17 @@ msgstr ""
"Organisatie van {event}"
#: pretix/base/settings.py:2446 pretix/base/settings.py:2483
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid ""
#| "Hello {attendee_name},\n"
#| "\n"
#| "a ticket for {event} has been ordered for you.\n"
#| "\n"
#| "You can view the details and status of your ticket here:\n"
#| "{url}\n"
#| "\n"
#| "Best regards, \n"
#| "Your {event} team"
msgid ""
"Hello,\n"
"\n"
@@ -10346,9 +10389,9 @@ msgid ""
"Best regards, \n"
"Your {event} team"
msgstr ""
"Beste,\n"
"Beste {attendee_name},\n"
"\n"
"Uw ticket voor {event} is geaccordeerd.\n"
"Er is een ticket voor {event} voor u besteld.\n"
"\n"
"U kunt de details en status van uw ticket hier bekijken:\n"
"{url}\n"
@@ -17252,8 +17295,10 @@ msgid "Valid check-in"
msgstr "Alle check-ins"
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:67
#, fuzzy
#| msgid "Additional information"
msgid "Additional information required"
msgstr "Extra informatie vereist"
msgstr "Extra informatie"
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:69
msgid ""

View File

@@ -7,8 +7,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-21 11:46+0000\n"
"PO-Revision-Date: 2023-08-24 04:00+0000\n"
"Last-Translator: Alain <alain@waag.org>\n"
"PO-Revision-Date: 2021-10-29 02:00+0000\n"
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
"nl/>\n"
"Language: 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 4.18.2\n"
"X-Generator: Weblate 4.8\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -252,7 +252,7 @@ msgstr "Dit ticket is nog niet betaald. Wilt u toch doorgaan?"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
msgid "Additional information required"
msgstr "Extra informatie vereist"
msgstr "Extra informatie nodig"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "Valid ticket"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-27 11:49+0000\n"
"PO-Revision-Date: 2023-08-24 04:00+0000\n"
"Last-Translator: Alain <alain@waag.org>\n"
"PO-Revision-Date: 2023-07-16 22:00+0000\n"
"Last-Translator: Freek Engelbarts <freekengelbarts@gmail.com>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_Informal/>\n"
"Language: 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 4.18.2\n"
"X-Generator: Weblate 4.17\n"
#: pretix/_base_settings.py:78
msgid "English"
@@ -555,8 +555,10 @@ msgid "Waiting list entry deleted"
msgstr "Wachtlijstitem"
#: pretix/api/webhooks.py:351
#, fuzzy
#| msgid "Waiting list entries"
msgid "Waiting list entry received voucher"
msgstr "Wachtlijstitem heeft voucher ontvangen"
msgstr "Wachtlijstitems"
#: pretix/base/addressvalidation.py:100 pretix/base/addressvalidation.py:103
#: pretix/base/addressvalidation.py:108 pretix/base/forms/questions.py:938

View File

@@ -34,7 +34,7 @@ class RuleSerializer(I18nAwareModelSerializer):
class Meta:
model = Rule
fields = ['id', 'subject', 'template', 'all_products', 'limit_products', 'restrict_to_status',
'checked_in_status', 'send_date', 'send_offset_days', 'send_offset_time', 'date_is_absolute',
'send_date', 'send_offset_days', 'send_offset_time', 'date_is_absolute',
'offset_to_event_end', 'offset_is_after', 'send_to', 'enabled']
read_only_fields = ['id']
@@ -88,10 +88,6 @@ class RuleSerializer(I18nAwareModelSerializer):
]:
raise ValidationError(f'status {s} not allowed: restrict_to_status may only include valid states')
if full_data.get('checked_in_status') == "":
# even though "blank" is not allowed on this field, "" gets accepted without this check
raise ValidationError('empty string not allowed: use null to disable check-in based filtering')
return full_data
def save(self, **kwargs):

View File

@@ -312,7 +312,7 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
fields = ['subject', 'template', 'attach_ical',
'send_date', 'send_offset_days', 'send_offset_time',
'all_products', 'limit_products', 'restrict_to_status',
'checked_in_status', 'send_to', 'enabled']
'send_to', 'enabled']
field_classes = {
'subevent': SafeModelMultipleChoiceField,
@@ -337,7 +337,6 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
'data-inverse-dependency': '#id_all_products'},
),
'send_to': forms.RadioSelect,
'checked_in_status': forms.RadioSelect,
}
def __init__(self, *args, **kwargs):

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.19 on 2023-08-09 11:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sendmail', '0004_rule_restrict_to_status'),
]
operations = [
migrations.AddField(
model_name='rule',
name='checked_in_status',
field=models.CharField(max_length=10, null=True),
),
]

View File

@@ -34,8 +34,7 @@ from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import (
Checkin, Event, InvoiceAddress, Item, Order, OrderPosition, SubEvent,
fields,
Event, InvoiceAddress, Item, Order, OrderPosition, SubEvent, fields,
)
from pretix.base.models.base import LoggingMixin
from pretix.base.services.mail import SendMailException
@@ -113,30 +112,19 @@ class ScheduledMail(models.Model):
e = self.event
orders = e.orders.all()
filter_orders_by_op = False
op_qs = OrderPosition.objects.filter(
order__event=self.event,
canceled=False,
)
limit_products = self.rule.limit_products.values_list('pk', flat=True) if not self.rule.all_products else None
if self.subevent:
filter_orders_by_op = True
op_qs = op_qs.filter(subevent=self.subevent)
orders = orders.filter(
Exists(OrderPosition.objects.filter(order=OuterRef('pk'), subevent=self.subevent))
)
elif e.has_subevents:
return # This rule should not even exist
if not self.rule.all_products:
filter_orders_by_op = True
limit_products = self.rule.limit_products.values_list('pk', flat=True)
op_qs = op_qs.filter(item_id__in=limit_products)
if self.rule.checked_in_status == "no_checkin":
filter_orders_by_op = True
op_qs = op_qs.filter(~Exists(Checkin.objects.filter(position_id=OuterRef('pk'))))
elif self.rule.checked_in_status == "checked_in":
filter_orders_by_op = True
op_qs = op_qs.filter(Exists(Checkin.objects.filter(position_id=OuterRef('pk'))))
orders = orders.filter(
Exists(OrderPosition.objects.filter(order=OuterRef('pk'), item_id__in=limit_products))
)
status_q = Q(status__in=self.rule.restrict_to_status)
if 'n__pending_approval' in self.rule.restrict_to_status:
@@ -154,8 +142,6 @@ class ScheduledMail(models.Model):
pk__gt=self.last_successful_order_id
)
if filter_orders_by_op:
orders = orders.filter(pk__in=op_qs.values_list('order_id', flat=True))
orders = orders.filter(
status_q,
).order_by('pk').select_related('invoice_address').prefetch_related('positions')
@@ -219,12 +205,6 @@ class Rule(models.Model, LoggingMixin):
(BOTH, _('Both (all order contact addresses and all attendee email addresses)'))
]
CHECK_IN_STATUS_CHOICES = [
(None, _("Everyone")),
("checked_in", _("Anyone who is or was checked in")),
("no_checkin", _("Anyone who never checked in before"))
]
id = models.BigAutoField(primary_key=True)
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='sendmail_rules')
@@ -239,15 +219,6 @@ class Rule(models.Model, LoggingMixin):
default=['p', 'n__valid_if_pending'],
)
checked_in_status = models.CharField(
verbose_name=_("Restrict to check-in status"),
default=None,
choices=CHECK_IN_STATUS_CHOICES,
max_length=10,
null=True,
blank=True,
)
attach_ical = models.BooleanField(
default=False,
verbose_name=_("Attach calendar files"),

View File

@@ -28,8 +28,6 @@
<legend>{% trans "Recipients" %}</legend>
{% bootstrap_field form.send_to layout='control' %}
{% bootstrap_field form.restrict_to_status layout='control' %}
{% bootstrap_field form.checked_in_status layout='control' %}
<hr>
{% bootstrap_field form.all_products layout='control' %}
{% bootstrap_field form.limit_products layout='horizontal' %}
</fieldset>

View File

@@ -42,8 +42,6 @@
<legend>{% trans "Recipients" %}</legend>
{% bootstrap_field form.send_to layout='control' %}
{% bootstrap_field form.restrict_to_status layout='control' %}
{% bootstrap_field form.checked_in_status layout='control' %}
<hr>
{% bootstrap_field form.all_products layout='control' %}
{% bootstrap_field form.limit_products layout='horizontal' %}
</fieldset>

View File

@@ -554,6 +554,9 @@ class StripeMethod(BasePaymentProvider):
ctx = {'request': request, 'event': self.event, 'settings': self.settings, 'provider': self}
return template.render(ctx)
def payment_can_retry(self, payment):
return self._is_still_available(order=payment.order)
def _charge_source(self, request, source, payment):
try:
params = {}
@@ -1578,6 +1581,9 @@ class StripeSofort(StripeMethod):
return True
return False
def payment_can_retry(self, payment):
return payment.state != OrderPayment.PAYMENT_STATE_PENDING and self._is_still_available(order=payment.order)
def payment_presale_render(self, payment: OrderPayment) -> str:
pi = payment.info_data or {}
try:

View File

@@ -39,13 +39,10 @@ from decimal import Decimal
from django.conf import settings
from django.contrib import messages
from django.core.cache import caches
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.signing import BadSignature, loads
from django.core.validators import EmailValidator
from django.db import models
from django.db.models import Count, F, Q, Sum
from django.db.models.functions import Cast
from django.db.models import F, Q
from django.http import HttpResponseNotAllowed, JsonResponse
from django.shortcuts import redirect
from django.utils import translation
@@ -65,14 +62,12 @@ from pretix.base.services.cart import (
)
from pretix.base.services.memberships import validate_memberships_in_order
from pretix.base.services.orders import perform_order
from pretix.base.services.tasks import EventTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import validate_cart_addons
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.phone_format import phone_format
from pretix.base.templatetags.rich_text import rich_text_snippet
from pretix.base.views.tasks import AsyncAction
from pretix.celery_app import app
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.forms.checkout import (
ContactForm, InvoiceAddressForm, InvoiceNameForm, MembershipForm,
@@ -807,9 +802,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
@cached_property
def invoice_form(self):
wd = self.cart_session.get('widget_data', {})
if self.invoice_address.pk:
wd_initial = {}
elif wd:
if not self.invoice_address.pk:
wd_initial = {
'name_parts': {
k[21:].replace('-', '_'): v
@@ -824,9 +817,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
'country': wd.get('invoice-address-country', ''),
}
else:
wd_initial = {
'is_business': self._get_is_business_heuristic(),
}
wd_initial = {}
initial = dict(wd_initial)
if self.cart_customer:
@@ -1035,25 +1026,6 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
ctx['cart_session'] = self.cart_session
ctx['invoice_address_asked'] = self.address_asked
def reduce_initial(v):
if isinstance(v, dict):
# try to flatten objects such as name_parts to a single string to determine whether they have any value set
return ''.join([v for k, v in v.items() if not k.startswith('_')])
else:
return v
def is_form_filled(form, ignore_keys=()):
return any([reduce_initial(v) for k, v in form.initial.items() if k not in ignore_keys])
ctx['invoice_address_open'] = (
self.request.event.settings.invoice_address_required or
self.request.event.settings.invoice_name_required or
'invoice' in self.request.GET or
# Checking for self.invoice_address.pk is not enough as when an invoice_address has been added and later edited to be empty, its not None.
# So check initial values as invoice_form can receive pre-filled values from invoice_address, widget-data or overwrites from plug-ins.
is_form_filled(self.invoice_form, ignore_keys=('is_business', 'country'))
)
if self.cart_customer:
if self.address_asked:
addresses = self.cart_customer.stored_addresses.all()
@@ -1142,31 +1114,6 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
ctx['profiles_data'] = profiles_list
return ctx
def _get_is_business_heuristic(self):
key = 'checkout_heuristic_is_business:' + str(self.event.pk)
cached_result = caches['default'].get(key)
if cached_result is None:
if caches['default'].add(key, False, timeout=10): # return False while query is running
QuestionsStep._update_is_business_heuristic.apply_async(args=(self.event.pk,))
return False
else:
return cached_result
@staticmethod
@app.task(base=EventTask)
def _update_is_business_heuristic(event):
result = InvoiceAddress.objects.filter(order__event=event).aggregate(
total=Count('*'), business=Sum(Cast('is_business', output_field=models.IntegerField())))
if result['total'] < 100:
result = InvoiceAddress.objects.filter(order__event__organizer=event.organizer).aggregate(
total=Count('*'), business=Sum(Cast('is_business', output_field=models.IntegerField())))
if result['business'] and result['total']:
is_business = result['business'] / result['total'] >= 0.6
else:
is_business = False
key = 'checkout_heuristic_is_business:' + str(event.pk)
caches['default'].set(key, is_business, timeout=12 * 3600) # 12 hours
class PaymentStep(CartMixin, TemplateFlowStep):
priority = 200

View File

@@ -76,7 +76,7 @@
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Invoice information" %}
<a href="{% eventurl request.event "presale:event.checkout" step="questions" cart_namespace=cart_namespace|default_if_none:"" %}?invoice=1#invoice-details" aria-label="{% trans "Modify invoice information" %}" class="h6">
<a href="{% eventurl request.event "presale:event.checkout" step="questions" cart_namespace=cart_namespace|default_if_none:"" %}?invoice=1" aria-label="{% trans "Modify invoice information" %}" class="h6">
<span class="fa fa-edit" aria-hidden="true"></span>{% trans "Modify" %}
</a>
</h3>

View File

@@ -34,7 +34,7 @@
</div>
</details>
{% if invoice_address_asked %}
<details class="panel panel-default" {% if invoice_address_open %}open{% endif %} id="invoice-details">
<details class="panel panel-default" {% if event.settings.invoice_address_required or event.settings.invoice_name_required %}open{% endif %}>
<summary class="panel-heading">
<h3 class="panel-title">
<strong>{% trans "Invoice information" %}{% if not event.settings.invoice_address_required and not event.settings.invoice_name_required %}

View File

@@ -496,12 +496,7 @@ class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView):
ctx['order'] = self.order
ctx['payment'] = self.payment
if 'order' in inspect.signature(self.payment.payment_provider.checkout_confirm_render).parameters:
if 'info_data' in inspect.signature(self.payment.payment_provider.checkout_confirm_render).parameters:
ctx['payment_info'] = self.payment.payment_provider.checkout_confirm_render(
self.request, order=self.order, info_data=self.payment.info_data
)
else:
ctx['payment_info'] = self.payment.payment_provider.checkout_confirm_render(self.request, order=self.order)
ctx['payment_info'] = self.payment.payment_provider.checkout_confirm_render(self.request, order=self.order)
else:
ctx['payment_info'] = self.payment.payment_provider.checkout_confirm_render(self.request)
ctx['payment_provider'] = self.payment.payment_provider

View File

@@ -8,8 +8,8 @@
"description": "List of rules, executed in order until one matches",
"properties": {
"country": {
"description": "Country code to match. ZZ = any country, EU = any EU country. For selected countries, a state can be matched (e.g. US-NY for New York).",
"enum": ["ZZ", "EU", "AF", "EG", "AX", "AL", "DZ", "AS", "VI", "AD", "AO", "AI", "AQ", "AG", "GQ", "AR", "AM", "AW", "AZ", "ET", "AU", "AU-ACT", "AU-NSW", "AU-NT", "AU-QLD", "AU-SA", "AU-TAS", "AU-VIC", "AU-WA", "BS", "BH", "BD", "BB", "BE", "BZ", "BJ", "BM", "BT", "BO", "BQ", "BA", "BW", "BV", "BR", "BR-AC", "BR-AL", "BR-AP", "BR-AM", "BR-BA", "BR-CE", "BR-ES", "BR-GO", "BR-MA", "BR-MT", "BR-MS", "BR-MG", "BR-PR", "BR-PB", "BR-PA", "BR-PE", "BR-PI", "BR-RN", "BR-RS", "BR-RJ", "BR-RO", "BR-RR", "BR-SC", "BR-SE", "BR-SP", "BR-TO", "VG", "IO", "BN", "BG", "BF", "BI", "CL", "CN", "MP", "CK", "CR", "CI", "CW", "DK", "DE", "DM", "DO", "DJ", "EC", "SV", "ER", "EE", "FK", "FO", "FJ", "FI", "FR", "GF", "PF", "TF", "GA", "GM", "GE", "GH", "GI", "GD", "GR", "GL", "GP", "GU", "GT", "GG", "GN", "GW", "GY", "HT", "HM", "HN", "HK", "IN", "ID", "IQ", "IR", "IE", "IS", "IM", "IL", "IT", "JM", "JP", "YE", "JE", "JO", "KY", "KH", "CM", "CA", "CA-AB", "CA-BC", "CA-MB", "CA-NB", "CA-NL", "CA-NT", "CA-NS", "CA-NU", "CA-ON", "CA-PE", "CA-QC", "CA-SK", "CA-YT", "CV", "KZ", "QA", "KE", "KG", "KI", "CC", "CO", "KM", "CG", "CD", "HR", "CU", "KW", "LA", "LS", "LV", "LB", "LR", "LY", "LI", "LT", "LU", "MO", "MG", "MW", "MY", "MY-01", "MY-02", "MY-03", "MY-04", "MY-05", "MY-06", "MY-08", "MY-09", "MY-07", "MY-12", "MY-13", "MY-10", "MY-11", "MV", "ML", "MT", "MA", "MH", "MQ", "MR", "MU", "YT", "MK", "MX", "MX-AGU", "MX-BCN", "MX-BCS", "MX-CAM", "MX-CHP", "MX-CHH", "MX-CMX", "MX-COA", "MX-COL", "MX-DUR", "MX-GUA", "MX-GRO", "MX-HID", "MX-JAL", "MX-MIC", "MX-MOR", "MX-MEX", "MX-NAY", "MX-NLE", "MX-OAX", "MX-PUE", "MX-QUE", "MX-ROO", "MX-SLP", "MX-SIN", "MX-SON", "MX-TAB", "MX-TAM", "MX-TLA", "MX-VER", "MX-YUC", "MX-ZAC", "FM", "MD", "MC", "MN", "ME", "MS", "MZ", "MM", "NA", "NR", "NP", "NC", "NZ", "NI", "NL", "NE", "NG", "NU", "KP", "NF", "NO", "OM", "AT", "TL", "PK", "PS", "PW", "PA", "PG", "PY", "PE", "PH", "PN", "PL", "PT", "PR", "RE", "RW", "RO", "RU", "BL", "PM", "SB", "ZM", "WS", "SM", "ST", "SA", "SE", "CH", "SN", "RS", "SC", "SL", "ZW", "SG", "SX", "SK", "SI", "SO", "ES", "SJ", "LK", "SH", "KN", "LC", "MF", "VC", "ZA", "SD", "GS", "KR", "SS", "SR", "SZ", "SY", "TJ", "TW", "TZ", "TH", "TG", "TK", "TO", "TT", "TD", "CZ", "TN", "TR", "TM", "TC", "TV", "UG", "UA", "HU", "UY", "UM", "UZ", "VU", "VA", "VE", "AE", "US", "US-AL", "US-AK", "US-AS", "US-AZ", "US-AR", "US-CA", "US-CO", "US-CT", "US-DE", "US-DC", "US-FL", "US-GA", "US-GU", "US-HI", "US-ID", "US-IL", "US-IN", "US-IA", "US-KS", "US-KY", "US-LA", "US-ME", "US-MD", "US-MA", "US-MI", "US-MN", "US-MS", "US-MO", "US-MT", "US-NE", "US-NV", "US-NH", "US-NJ", "US-NM", "US-NY", "US-NC", "US-ND", "US-MP", "US-OH", "US-OK", "US-OR", "US-PA", "US-PR", "US-RI", "US-SC", "US-SD", "US-TN", "US-TX", "US-UM", "US-UT", "US-VT", "US-VI", "US-VA", "US-WA", "US-WV", "US-WI", "US-WY", "GB", "VN", "WF", "CX", "BY", "EH", "CF", "CY"]
"description": "Country code to match. ZZ = any country, EU = any EU country.",
"enum": ["AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "EU", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW", "ZZ"]
},
"address_type": {
"description": "Type of customer, emtpy = any.",

View File

@@ -39,8 +39,7 @@ TEST_RULE_RES = {
'template': {'en': 'foo'},
'all_products': True,
'limit_products': [],
'restrict_to_status': ['p', 'n__valid_if_pending'],
'checked_in_status': None,
"restrict_to_status": ['p', 'n__valid_if_pending'],
'send_date': '2021-07-08T00:00:00Z',
'send_offset_days': None,
'send_offset_time': None,
@@ -161,8 +160,7 @@ def test_sendmail_rule_create_full(token_client, organizer, event, item):
'template': {'en': 'foobar'},
'all_products': False,
'limit_products': [event.items.first().pk],
'restrict_to_status': ['p', 'n__not_pending_approval_and_not_valid_if_pending', 'n__valid_if_pending'],
'checked_in_status': None,
"restrict_to_status": ['p', 'n__not_pending_approval_and_not_valid_if_pending', 'n__valid_if_pending'],
'send_offset_days': 3,
'send_offset_time': '09:30',
'date_is_absolute': False,
@@ -176,7 +174,6 @@ def test_sendmail_rule_create_full(token_client, organizer, event, item):
assert r.all_products is False
assert [i.pk for i in r.limit_products.all()] == [event.items.first().pk]
assert r.restrict_to_status == ['p', 'n__not_pending_approval_and_not_valid_if_pending', 'n__valid_if_pending']
assert r.checked_in_status is None
assert r.send_offset_days == 3
assert r.send_offset_time == datetime.time(9, 30)
assert r.date_is_absolute is False
@@ -351,49 +348,6 @@ def test_sendmail_rule_restrict_recipients(token_client, organizer, event, rule)
)
@scopes_disabled()
@pytest.mark.django_db
def test_sendmail_rule_checkin(token_client, organizer, event, rule):
valid_states = [None, 'checked_in', 'no_checkin', ]
invalid_states = ['', 'foo']
for s in valid_states:
result = create_rule(
token_client, organizer, event,
data={
'subject': {'en': 'meow'},
'template': {'en': 'creative text here'},
'send_date': '2018-03-17T13:31Z',
'checked_in_status': s,
},
expected_failure=False
)
assert result.checked_in_status == s
for s in invalid_states:
create_rule(
token_client, organizer, event,
data={
'subject': {'en': 'meow'},
'template': {'en': 'creative text here'},
'send_date': '2018-03-17T13:31Z',
'checked_in_status': s,
},
expected_failure=True
)
result = create_rule(
token_client, organizer, event,
data={
'subject': {'en': 'meow'},
'template': {'en': 'creative text here'},
'send_date': '2018-03-17T13:31Z',
},
expected_failure=False
)
assert result.checked_in_status is None
@scopes_disabled()
@pytest.mark.django_db
def test_sendmail_rule_change(token_client, organizer, event, rule):

View File

@@ -97,15 +97,7 @@ def test_payment_fee_reverse_percent_and_abs_default(event):
def test_availability_date_available(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', datetime.date.today() + datetime.timedelta(days=1))
result = prov._is_available_by_time()
assert result
@pytest.mark.django_db
def test_availability_start_available(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_start', datetime.date.today() - datetime.timedelta(days=1))
result = prov._is_available_by_time()
result = prov._is_still_available()
assert result
@@ -113,15 +105,7 @@ def test_availability_start_available(event):
def test_availability_date_not_available(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', datetime.date.today() - datetime.timedelta(days=1))
result = prov._is_available_by_time()
assert not result
@pytest.mark.django_db
def test_availability_start_not_available(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_start', datetime.date.today() + datetime.timedelta(days=1))
result = prov._is_available_by_time()
result = prov._is_still_available()
assert not result
@@ -137,26 +121,9 @@ def test_availability_date_relative(event):
))
utc = datetime.timezone.utc
assert prov._is_available_by_time(datetime.datetime(2016, 11, 30, 23, 0, 0, tzinfo=tz).astimezone(utc))
assert prov._is_available_by_time(datetime.datetime(2016, 12, 1, 23, 59, 0, tzinfo=tz).astimezone(utc))
assert not prov._is_available_by_time(datetime.datetime(2016, 12, 2, 0, 0, 1, tzinfo=tz).astimezone(utc))
@pytest.mark.django_db
def test_availability_start_relative(event):
event.settings.set('timezone', 'US/Pacific')
tz = ZoneInfo('US/Pacific')
event.date_from = datetime.datetime(2016, 12, 3, 12, 0, 0, tzinfo=tz)
event.save()
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_start', RelativeDateWrapper(
RelativeDate(days_before=2, time=datetime.time(12, 0), base_date_name='date_from', minutes_before=None)
))
utc = datetime.timezone.utc
assert not prov._is_available_by_time(datetime.datetime(2016, 11, 30, 23, 0, 0, tzinfo=tz).astimezone(utc))
assert prov._is_available_by_time(datetime.datetime(2016, 12, 1, 0, 0, tzinfo=tz).astimezone(utc))
assert prov._is_available_by_time(datetime.datetime(2016, 12, 2, 0, 0, 1, tzinfo=tz).astimezone(utc))
assert prov._is_still_available(datetime.datetime(2016, 11, 30, 23, 0, 0, tzinfo=tz).astimezone(utc))
assert prov._is_still_available(datetime.datetime(2016, 12, 1, 23, 59, 0, tzinfo=tz).astimezone(utc))
assert not prov._is_still_available(datetime.datetime(2016, 12, 2, 0, 0, 1, tzinfo=tz).astimezone(utc))
@pytest.mark.django_db
@@ -167,9 +134,9 @@ def test_availability_date_timezones(event):
tz = ZoneInfo('US/Pacific')
utc = ZoneInfo('UTC')
assert prov._is_available_by_time(datetime.datetime(2016, 11, 30, 23, 0, 0, tzinfo=tz).astimezone(utc))
assert prov._is_available_by_time(datetime.datetime(2016, 12, 1, 23, 59, 0, tzinfo=tz).astimezone(utc))
assert not prov._is_available_by_time(datetime.datetime(2016, 12, 2, 0, 0, 1, tzinfo=tz).astimezone(utc))
assert prov._is_still_available(datetime.datetime(2016, 11, 30, 23, 0, 0, tzinfo=tz).astimezone(utc))
assert prov._is_still_available(datetime.datetime(2016, 12, 1, 23, 59, 0, tzinfo=tz).astimezone(utc))
assert not prov._is_still_available(datetime.datetime(2016, 12, 2, 0, 0, 1, tzinfo=tz).astimezone(utc))
@pytest.mark.django_db
@@ -195,12 +162,12 @@ def test_availability_date_cart_relative_subevents(event):
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days_before=3, time=None, base_date_name='date_from', minutes_before=None)
))
assert prov._is_available_by_time(cart_id="123")
assert prov._is_still_available(cart_id="123")
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days_before=4, time=None, base_date_name='date_from', minutes_before=None)
))
assert not prov._is_available_by_time(cart_id="123")
assert not prov._is_still_available(cart_id="123")
@pytest.mark.django_db
@@ -234,9 +201,9 @@ def test_availability_date_order_relative_subevents(event):
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days_before=3, time=None, base_date_name='date_from', minutes_before=None)
))
assert prov._is_available_by_time(order=order)
assert prov._is_still_available(order=order)
prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days_before=4, time=None, base_date_name='date_from', minutes_before=None)
))
assert not prov._is_available_by_time(order=order)
assert not prov._is_still_available(order=order)

View File

@@ -477,81 +477,6 @@ def test_custom_rules_specific_country(event):
)
@pytest.mark.django_db
def test_custom_rules_specific_state(event):
tr = TaxRule(
event=event,
rate=Decimal('10.00'), price_includes_tax=False,
custom_rules=json.dumps([
{'country': 'US-NY', 'address_type': '', 'action': 'vat', 'rate': '20.00'},
{'country': 'US-DE', 'address_type': '', 'action': 'no'},
{'country': 'US', 'address_type': '', 'action': 'vat', 'rate': '30.00'},
])
)
ia = InvoiceAddress(
is_business=True,
country=Country('DE')
)
assert not tr.is_reverse_charge(ia)
assert tr._tax_applicable(ia)
assert tr.tax_rate_for(ia) == Decimal('10.00')
assert tr.tax(Decimal('100.00'), invoice_address=ia) == TaxedPrice(
gross=Decimal('110.00'),
net=Decimal('100.00'),
tax=Decimal('10.00'),
rate=Decimal('10.00'),
name='',
)
ia = InvoiceAddress(
is_business=True,
country=Country('US'),
state='NC'
)
assert not tr.is_reverse_charge(ia)
assert tr._tax_applicable(ia)
assert tr.tax_rate_for(ia) == Decimal('30.00')
assert tr.tax(Decimal('100.00'), invoice_address=ia) == TaxedPrice(
gross=Decimal('130.00'),
net=Decimal('100.00'),
tax=Decimal('30.00'),
rate=Decimal('30.00'),
name='',
)
ia = InvoiceAddress(
is_business=True,
country=Country('US'),
state='NY'
)
assert not tr.is_reverse_charge(ia)
assert tr._tax_applicable(ia)
assert tr.tax_rate_for(ia) == Decimal('20.00')
assert tr.tax(Decimal('100.00'), invoice_address=ia) == TaxedPrice(
gross=Decimal('120.00'),
net=Decimal('100.00'),
tax=Decimal('20.00'),
rate=Decimal('20.00'),
name='',
)
ia = InvoiceAddress(
is_business=True,
country=Country('US'),
state='DE'
)
assert not tr.is_reverse_charge(ia)
assert not tr._tax_applicable(ia)
assert tr.tax_rate_for(ia) == Decimal('0.00')
assert tr.tax(Decimal('100.00'), invoice_address=ia) == TaxedPrice(
gross=Decimal('100.00'),
net=Decimal('100.00'),
tax=Decimal('0.00'),
rate=Decimal('0.00'),
name='',
)
@pytest.mark.django_db
def test_custom_rules_individual(event):
tr = TaxRule(

View File

@@ -29,7 +29,6 @@ from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import InvoiceAddress, Order
from pretix.base.services.checkin import perform_checkin
from pretix.plugins.sendmail.models import Rule, ScheduledMail
from pretix.plugins.sendmail.signals import sendmail_run_rules
@@ -277,100 +276,6 @@ def test_sendmail_rule_send_correct_products(event, order, item, item2):
assert djmail.outbox[0].to[0] == p1.attendee_email
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_not_checked_in_all_get_mail(event, order, item):
order.status = Order.STATUS_PAID
order.save()
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="all",
subject='meow', template='meow meow meow')
djmail.outbox = []
sendmail_run_rules(None)
assert len(djmail.outbox) == 1, "email not sent"
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_checked_in_all_get_mail(event, order, item):
order.status = Order.STATUS_PAID
order.save()
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
clist = event.checkin_lists.create(name="Default", all_products=True)
perform_checkin(p1, clist, {})
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="all",
subject='meow', template='meow meow meow')
djmail.outbox = []
sendmail_run_rules(None)
assert len(djmail.outbox) == 1, "email not sent"
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_not_checked_in_no_mail(event, order, item):
order.status = Order.STATUS_PAID
order.save()
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="checked_in",
subject='meow', template='meow meow meow')
# receives no mail when not checked in
djmail.outbox = []
sendmail_run_rules(None)
assert len(djmail.outbox) == 0, "email sent unexpectedly"
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_not_checked_in_get_mail(event, order, item):
order.status = Order.STATUS_PAID
order.save()
order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="no_checkin",
subject='meow', template='meow meow meow')
# receives mail when not checked in
djmail.outbox = []
sendmail_run_rules(None)
assert len(djmail.outbox) == 1, "email not sent"
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_checked_in_no_mail(event, order, item):
order.status = Order.STATUS_PAID
order.save()
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
clist = event.checkin_lists.create(name="Default", all_products=True)
# receives no mail when checked in
djmail.outbox = []
perform_checkin(p1, clist, {})
assert clist.checkin_count == 1
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="no_checkin",
subject='meow', template='meow meow meow')
sendmail_run_rules(None)
assert len(djmail.outbox) == 0, "email sent unexpectedly"
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_checked_in_get_mail(event, order, item):
order.status = Order.STATUS_PAID
order.save()
p1 = order.all_positions.create(item=item, price=13, attendee_email='item1@dummy.test')
clist = event.checkin_lists.create(name="Default", all_products=True)
# receives mail when checked in
djmail.outbox = []
perform_checkin(p1, clist, {})
assert clist.checkin_count == 1
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), checked_in_status="checked_in",
subject='meow', template='meow meow meow')
sendmail_run_rules(None)
assert len(djmail.outbox) == 1, "email not sent"
@pytest.mark.django_db
@scopes_disabled()
def run_restriction_test(event, order, restrictions_pass=[], restrictions_fail=[]):

View File

@@ -2414,25 +2414,6 @@ class CartAddonTest(CartTestMixin, TestCase):
assert cp2.item == self.workshop1
assert cp2.price == 0
@classscope(attr='orga')
def test_extend_included_addon_no_longer_available(self):
self.addon1.price_included = True
self.addon1.save()
self.quota_tickets.size = 0
self.quota_tickets.save()
cp1 = CartPosition.objects.create(
expires=now() - timedelta(minutes=10), item=self.ticket, price=Decimal('23.00'),
event=self.event, cart_id=self.session_key
)
CartPosition.objects.create(
expires=now() - timedelta(minutes=10), item=self.workshop1, price=Decimal('0.00'),
event=self.event, cart_id=self.session_key, addon_to=cp1
)
self.cm.extend_expired_positions()
with self.assertRaises(CartError):
self.cm.commit()
assert CartPosition.objects.count() == 0
@classscope(attr='orga')
def test_cart_addon_remove_parent(self):
self.addon1.price_included = True