Compare commits

..
Author SHA1 Message Date
Mira Weller feb2ec0d48 Adapt remaining tests 2024-08-27 22:01:05 +02:00
Mira Weller f3eb6b33e6 Adapt test cases 2024-08-27 22:01:05 +02:00
Mira Weller 3a774db641 Refactor 2024-08-27 22:01:05 +02:00
Mira Weller 6245d1f286 Show weekday in subevent dates 2024-08-27 22:01:04 +02:00
138 changed files with 36342 additions and 72554 deletions
+5 -36
View File
@@ -23,22 +23,6 @@ position integer An integer, use
is_addon boolean If ``true``, items within this category are not on sale
on their own but the category provides a source for
defining add-ons for other products.
cross_selling_mode string If ``null``, cross-selling is disabled for this category.
If ``"only"``, it is only visible in the cross-selling
step.
If ``"both"``, it is visible on the normal index page
as well.
Only available if ``is_addon`` is ``false``.
cross_selling_condition string Only relevant if ``cross_selling_mode`` is not ``null``.
If ``"always"``, always show in cross-selling step.
If ``"products"``, only show if the cart contains one of
the products listed in ``cross_selling_match_products``.
If ``"discounts"``, only show products that qualify for
a discount according to discount rules.
cross_selling_match_products list of integer Only relevant if ``cross_selling_condition`` is
``"products"``. Internal ID of the items of which at
least one needs to be in the cart for this category to
be shown.
===================================== ========================== =======================================================
@@ -76,10 +60,7 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
"is_addon": false
}
]
}
@@ -121,10 +102,7 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
"is_addon": false
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -152,10 +130,7 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
"is_addon": false
}
**Example response**:
@@ -172,10 +147,7 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": false,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
"is_addon": false
}
:param organizer: The ``slug`` field of the organizer of the event to create a category for
@@ -221,10 +193,7 @@ Endpoints
"internal_name": "",
"description": {"en": "Tickets are what you need to get in."},
"position": 1,
"is_addon": true,
"cross_selling_mode": null,
"cross_selling_condition": null,
"cross_selling_match_products": []
"is_addon": true
}
:param organizer: The ``slug`` field of the organizer to modify
+1 -2
View File
@@ -203,8 +203,7 @@ checkins list of objects List of **succe
├ datetime datetime Time of check-in
├ type string Type of scan (defaults to ``entry``)
├ gate integer Internal ID of the gate. Can be ``null``.
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
├ device_id integer Attribute ``device_id`` of the device. Can be ``null``.
├ device integer Internal ID of the device. Can be ``null``.
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
downloads list of objects List of ticket download options
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
+2 -3
View File
@@ -20,9 +20,8 @@ internal_name string An optional nam
rate decimal (string) Tax rate in percent
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
the specified product price
eu_reverse_charge boolean **DEPRECATED**. If ``true``, EU reverse charge rules
are applied. Will be ignored if custom rules are set.
Use custom rules instead.
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied. Will
be ignored if custom rules are set.
home_country string Merchant country (required for reverse charge), can be
``null`` or empty string
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
+1 -1
View File
@@ -175,7 +175,7 @@ without any special behavior.
Connecting SSO providers (pretix as the SSO client)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To connect an external application as a SSO provider, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
To connect an external application as a SSO client, go to "Customer accounts" → "SSO providers" → "Create a new SSO provider"
in your organizer account.
.. thumbnail:: ../../screens/organizer/customer_ssoprovider_add.png
+4 -4
View File
@@ -36,7 +36,7 @@ dependencies = [
"css-inline==0.14.*",
"defusedcsv>=1.1.0",
"Django[argon2]==4.2.*,>=4.2.15",
"django-bootstrap3==24.3",
"django-bootstrap3==24.2",
"django-compressor==4.5.1",
"django-countries==7.6.*",
"django-filter==24.3",
@@ -76,12 +76,12 @@ dependencies = [
"phonenumberslite==8.13.*",
"Pillow==10.4.*",
"pretix-plugin-build",
"protobuf==5.28.*",
"protobuf==5.27.*",
"psycopg2-binary",
"pycountry",
"pycparser==2.22",
"pycryptodome==3.20.*",
"pypdf==5.0.*",
"pypdf==4.3.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*",
"pytz",
@@ -91,7 +91,7 @@ dependencies = [
"redis==5.0.*",
"reportlab==4.2.*",
"requests==2.31.*",
"sentry-sdk==2.14.*",
"sentry-sdk==2.13.*",
"sepaxml==2.6.*",
"slimit",
"stripe==7.9.*",
+1 -1
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__ = "2024.10.0.dev0"
__version__ = "2024.8.0.dev0"
-1
View File
@@ -80,7 +80,6 @@ ALL_LANGUAGES = [
('de', _('German')),
('de-informal', _('German (informal)')),
('ar', _('Arabic')),
('eu', _('Basque')),
('ca', _('Catalan')),
('zh-hans', _('Chinese (simplified)')),
('zh-hant', _('Chinese (traditional)')),
-1
View File
@@ -896,7 +896,6 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
'locale',
'last_order_modification_date',
'show_quota_left',
'show_dates_on_frontpage',
'max_items_per_order',
'attendee_names_asked',
'attendee_names_required',
+1 -16
View File
@@ -441,22 +441,7 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
class Meta:
model = ItemCategory
fields = (
'id', 'name', 'internal_name', 'description', 'position',
'is_addon', 'cross_selling_mode',
'cross_selling_condition', 'cross_selling_match_products'
)
def validate(self, data):
data = super().validate(data)
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
if full_data.get('is_addon') and full_data.get('cross_selling_mode'):
raise ValidationError('is_addon and cross_selling_mode are mutually exclusive')
return data
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon')
class QuestionOptionSerializer(I18nAwareModelSerializer):
+1 -7
View File
@@ -273,15 +273,9 @@ class AnswerSerializer(I18nAwareModelSerializer):
class CheckinSerializer(I18nAwareModelSerializer):
device_id = serializers.SlugRelatedField(
source='device',
slug_field='device_id',
read_only=True,
)
class Meta:
model = Checkin
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'type')
class FailedCheckinSerializer(I18nAwareModelSerializer):
+1 -1
View File
@@ -377,7 +377,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
Prefetch(
'positions',
OrderPosition.objects.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('checkins', queryset=Checkin.objects.all()),
'item', 'variation', 'answers', 'answers__options', 'answers__question',
)
)
+1 -1
View File
@@ -78,7 +78,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
queryset=OrderPosition.objects.select_related(
'order', 'order__event', 'order__event__organizer', 'seat',
).prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('checkins', queryset=Checkin.objects.all()),
'answers', 'answers__options', 'answers__question',
)
),
+5 -5
View File
@@ -258,7 +258,7 @@ class OrderViewSetMixin:
return Prefetch(
'positions',
opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
)),
@@ -279,7 +279,7 @@ class OrderViewSetMixin:
return Prefetch(
'positions',
opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('checkins', queryset=Checkin.objects.all()),
'item', 'variation',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
'seat',
@@ -1092,7 +1092,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
'item_meta_properties',
)
qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached')
@@ -1111,7 +1111,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
Prefetch(
'positions',
qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('checkins', queryset=Checkin.objects.all()),
Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached')
@@ -1135,7 +1135,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
)
else:
qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('checkins', queryset=Checkin.objects.all()),
'answers', 'answers__options', 'answers__question',
).select_related(
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
+1 -63
View File
@@ -32,16 +32,13 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import string
from collections import OrderedDict
from importlib import import_module
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth.hashers import check_password, make_password
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _, ngettext
from django.utils.translation import gettext_lazy as _
def get_auth_backends():
@@ -163,62 +160,3 @@ class NativeAuthBackend(BaseAuthBackend):
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
if u and u.auth_backend == self.identifier:
return u
class NumericAndAlphabeticPasswordValidator:
def validate(self, password, user=None):
has_numeric = any(c in string.digits for c in password)
has_alpha = any(c in string.ascii_letters for c in password)
if not has_numeric or not has_alpha:
raise ValidationError(
_(
"Your password must contain both numeric and alphabetic characters.",
),
code="password_numeric_and_alphabetic",
)
def get_help_text(self):
return _(
"Your password must contain both numeric and alphabetic characters.",
)
class HistoryPasswordValidator:
def __init__(self, history_length=4):
self.history_length = history_length
def validate(self, password, user=None):
from pretix.base.models import User
if not user or not user.pk or not isinstance(user, User):
return
for hp in user.historic_passwords.order_by("-created")[:self.history_length]:
if check_password(password, hp.password):
raise ValidationError(
ngettext(
"Your password may not be the same as your previous password.",
"Your password may not be the same as one of your %(history_length)s previous passwords.",
self.history_length,
),
code="password_history",
params={"history_length": self.history_length},
)
def get_help_text(self):
return ngettext(
"Your password may not be the same as your previous password.",
"Your password may not be the same as one of your %(history_length)s previous passwords.",
self.history_length,
) % {"history_length": self.history_length}
def password_changed(self, password, user=None):
if not user:
pass
user.historic_passwords.create(password=make_password(password))
user.historic_passwords.filter(
pk__in=user.historic_passwords.order_by("-created")[self.history_length:].values_list("pk", flat=True),
).delete()
+1 -33
View File
@@ -46,8 +46,6 @@ This module contains utilities for implementing OpenID Connect for customer auth
as well as an OpenID Provider (OP).
"""
pretix_token_endpoint_auth_methods = ['client_secret_basic', 'client_secret_post']
def _urljoin(base, path):
if not base.endswith("/"):
@@ -129,16 +127,6 @@ def oidc_validate_and_complete_config(config):
fields=", ".join(provider_config.get("claims_supported", []))
))
if "token_endpoint_auth_methods_supported" in provider_config:
token_endpoint_auth_methods_supported = provider_config.get("token_endpoint_auth_methods_supported",
["client_secret_basic"])
if not any(x in pretix_token_endpoint_auth_methods for x in token_endpoint_auth_methods_supported):
raise ValidationError(
_(f'No supported Token Endpoint Auth Methods supported: {token_endpoint_auth_methods_supported}').format(
token_endpoint_auth_methods_supported=", ".join(token_endpoint_auth_methods_supported)
)
)
config['provider_config'] = provider_config
return config
@@ -159,18 +147,6 @@ def oidc_authorize_url(provider, state, redirect_uri):
def oidc_validate_authorization(provider, code, redirect_uri):
endpoint = provider.configuration['provider_config']['token_endpoint']
# Wall of shame and RFC ignorant IDPs
if endpoint == 'https://www.linkedin.com/oauth/v2/accessToken':
token_endpoint_auth_method = 'client_secret_post'
else:
token_endpoint_auth_methods = provider.configuration['provider_config'].get(
'token_endpoint_auth_methods_supported', ['client_secret_basic']
)
token_endpoint_auth_method = [
x for x in pretix_token_endpoint_auth_methods if x in token_endpoint_auth_methods
][0]
params = {
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
# https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
@@ -178,11 +154,6 @@ def oidc_validate_authorization(provider, code, redirect_uri):
'code': code,
'redirect_uri': redirect_uri,
}
if token_endpoint_auth_method == 'client_secret_post':
params['client_id'] = provider.configuration['client_id']
params['client_secret'] = provider.configuration['client_secret']
try:
resp = requests.post(
endpoint,
@@ -190,10 +161,7 @@ def oidc_validate_authorization(provider, code, redirect_uri):
headers={
'Accept': 'application/json',
},
auth=(
provider.configuration['client_id'],
provider.configuration['client_secret']
) if token_endpoint_auth_method == 'client_secret_basic' else None,
auth=(provider.configuration['client_id'], provider.configuration['client_secret']),
)
resp.raise_for_status()
data = resp.json()
@@ -50,7 +50,6 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--tasks', action='store', type=str, help='Only execute the tasks with this name '
'(dotted path, comma separation)')
parser.add_argument('--list-tasks', action='store_true', help='Only list all tasks')
parser.add_argument('--exclude', action='store', type=str, help='Exclude the tasks with this name '
'(dotted path, comma separation)')
@@ -62,9 +61,6 @@ class Command(BaseCommand):
for receiver in periodic_task._live_receivers(self):
name = f'{receiver.__module__}.{receiver.__name__}'
if options['list_tasks']:
print(name)
continue
if options.get('tasks'):
if name not in options.get('tasks').split(','):
continue
@@ -1,36 +0,0 @@
# Generated by Django 4.2.15 on 2024-09-16 15:10
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0269_order_api_meta"),
]
operations = [
migrations.CreateModel(
name="HistoricPassword",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("created", models.DateTimeField(auto_now_add=True)),
("password", models.CharField(max_length=128)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="historic_passwords",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
@@ -1,32 +0,0 @@
# Generated by Django 4.2.11 on 2024-05-27 13:19
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0270_historicpassword"),
]
operations = [
migrations.AddField(
model_name="itemcategory",
name="cross_selling_condition",
field=models.CharField(null=True, max_length=10),
),
migrations.AddField(
model_name="itemcategory",
name="cross_selling_mode",
field=models.CharField(null=True, max_length=5),
),
migrations.AddField(
model_name="itemcategory",
name="cross_selling_match_products",
field=models.ManyToManyField(
related_name="matched_by_cross_selling_categories", to="pretixbase.item"
),
),
]
+1 -7
View File
@@ -213,13 +213,7 @@ class DatetimeColumnMixin:
except (ValueError, TypeError):
pass
else:
try:
d = datetime.datetime.fromisoformat(value)
if not d.tzinfo:
d = d.replace(tzinfo=self.timezone)
return d
except (ValueError, TypeError):
raise ValidationError(_("Could not parse {value} as a date and time.").format(value=value))
raise ValidationError(_("Could not parse {value} as a date and time.").format(value=value))
class DecimalColumnMixin:
+2 -20
View File
@@ -40,8 +40,8 @@ from phonenumbers import SUPPORTED_REGIONS
from pretix.base.forms.questions import guess_country
from pretix.base.modelimport import (
BooleanColumnMixin, DatetimeColumnMixin, DecimalColumnMixin, ImportColumn,
SubeventColumnMixin, i18n_flat,
DatetimeColumnMixin, DecimalColumnMixin, ImportColumn, SubeventColumnMixin,
i18n_flat,
)
from pretix.base.models import (
Customer, ItemVariation, OrderPosition, Question, QuestionAnswer,
@@ -604,22 +604,6 @@ class Comment(ImportColumn):
order.comment = value or ''
class CheckinAttentionColumn(BooleanColumnMixin, ImportColumn):
identifier = 'checkin_attention'
verbose_name = gettext_lazy('Requires special attention')
def assign(self, value, order, position, invoice_address, **kwargs):
order.checkin_attention = value
class CheckinTextColumn(ImportColumn):
identifier = 'checkin_text'
verbose_name = gettext_lazy('Check-in text')
def assign(self, value, order, position, invoice_address, **kwargs):
order.checkin_text = value
class QuestionColumn(ImportColumn):
def __init__(self, event, q):
self.q = q
@@ -758,8 +742,6 @@ def get_order_import_columns(event):
ValidUntil(event),
Locale(event),
Saleschannel(event),
CheckinAttentionColumn(event),
CheckinTextColumn(event),
Expires(event),
Comment(event),
]
-6
View File
@@ -654,9 +654,3 @@ class WebAuthnDevice(Device):
@property
def webauthnpubkey(self):
return websafe_decode(self.pub_key)
class HistoricPassword(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="historic_passwords")
created = models.DateTimeField(auto_now_add=True)
password = models.CharField(verbose_name=_("Password"), max_length=128)
+31 -70
View File
@@ -20,11 +20,11 @@
# <https://www.gnu.org/licenses/>.
#
from collections import defaultdict, namedtuple
from collections import defaultdict
from decimal import Decimal
from itertools import groupby
from math import ceil, inf
from typing import Dict
from math import ceil
from typing import Dict, Optional, Tuple
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
@@ -36,8 +36,6 @@ from django_scopes import ScopedManager
from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
PositionInfo = namedtuple('PositionInfo', ['item_id', 'subevent_id', 'line_price_gross', 'is_addon_to', 'voucher_discount'])
class Discount(LoggedModel):
SUBEVENT_MODE_MIXED = 'mixed'
@@ -247,26 +245,22 @@ class Discount(LoggedModel):
return False
return True
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
if self.condition_min_value and sum(positions[idx].line_price_gross for idx in condition_idx_group) < self.condition_min_value:
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result):
if self.condition_min_value and sum(positions[idx][2] for idx in condition_idx_group) < self.condition_min_value:
return
if self.condition_min_count or self.benefit_only_apply_to_cheapest_n_matches:
raise ValueError('Validation invariant violated.')
for idx in benefit_idx_group:
previous_price = positions[idx].line_price_gross
previous_price = positions[idx][2]
new_price = round_decimal(
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
self.event.currency,
)
result[idx] = new_price
if collect_potential_discounts is not None:
for idx in condition_idx_group:
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result):
if len(condition_idx_group) < self.condition_min_count:
return
@@ -274,53 +268,23 @@ class Discount(LoggedModel):
raise ValueError('Validation invariant violated.')
if self.benefit_only_apply_to_cheapest_n_matches:
# sort by line_price
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
if not self.condition_min_count:
raise ValueError('Validation invariant violated.')
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx][2], -idx)) # sort by line_price
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
# want to match multiples of 3
# how many discount applications are allowed according to condition products in cart
possible_applications_cond = len(condition_idx_group) // self.condition_min_count
# how many discount applications are possible according to benefitting products in cart
possible_applications_benefit = ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches)
n_groups = min(possible_applications_cond, possible_applications_benefit)
n_groups = min(len(condition_idx_group) // self.condition_min_count, ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches))
consume_idx = condition_idx_group[:n_groups * self.condition_min_count]
benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches]
if collect_potential_discounts is not None:
if n_groups * self.benefit_only_apply_to_cheapest_n_matches > len(benefit_idx_group):
# partially used discount ("for each 1 ticket you buy, get 50% on 2 t-shirts", cart content: 1 ticket
# but only 1 t-shirt) -> 1 shirt definitiv potential discount
for idx in consume_idx:
collect_potential_discounts[idx] = [
(self, n_groups * self.benefit_only_apply_to_cheapest_n_matches - len(benefit_idx_group), -1, subevent_id)
]
if possible_applications_cond * self.benefit_only_apply_to_cheapest_n_matches > len(benefit_idx_group):
# unused discount ("for each 1 ticket you buy, get 50% on 2 t-shirts", cart content: 1 ticket
# but 0 t-shirts) -> 2 shirt maybe potential discount (if the 1 ticket is not consumed by a later discount)
for i, idx in enumerate(condition_idx_group[
n_groups * self.condition_min_count:
possible_applications_cond * self.condition_min_count
]):
collect_potential_discounts[idx] += [
(self, self.benefit_only_apply_to_cheapest_n_matches, i // self.condition_min_count, subevent_id)
]
else:
consume_idx = condition_idx_group
benefit_idx = benefit_idx_group
if collect_potential_discounts is not None:
for idx in consume_idx:
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
for idx in benefit_idx:
previous_price = positions[idx].line_price_gross
previous_price = positions[idx][2]
new_price = round_decimal(
previous_price * (Decimal('100.00') - self.benefit_discount_matching_percent) / Decimal('100.00'),
self.event.currency,
@@ -328,16 +292,15 @@ class Discount(LoggedModel):
result[idx] = new_price
for idx in consume_idx:
result.setdefault(idx, positions[idx].line_price_gross)
result.setdefault(idx, positions[idx][2])
def apply(self, positions: Dict[int, PositionInfo],
collect_potential_discounts=None) -> Dict[int, Decimal]:
def apply(self, positions: Dict[int, Tuple[int, Optional[int], Decimal, bool, Decimal]]) -> Dict[int, Decimal]:
"""
Tries to apply this discount to a cart
:param positions: Dictionary mapping IDs to PositionInfo tuples.
:param positions: Dictionary mapping IDs to tuples of the form
``(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)``.
Bundled positions may not be included.
:param collect_potential_discounts: For detailed description, see pretix.base.services.pricing.apply_discounts
:return: A dictionary mapping keys from the input dictionary to new prices. All positions
contained in this dictionary are considered "consumed" and should not be considered
@@ -379,13 +342,13 @@ class Discount(LoggedModel):
if self.subevent_mode == self.SUBEVENT_MODE_MIXED: # also applies to non-series events
if self.condition_min_count:
self._apply_min_count(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
self._apply_min_count(positions, condition_candidates, benefit_candidates, result)
else:
self._apply_min_value(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
self._apply_min_value(positions, condition_candidates, benefit_candidates, result)
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
def key(idx):
return positions[idx].subevent_id or 0
return positions[idx][1] or 0 # subevent_id
# Build groups of candidates with the same subevent, then apply our regular algorithm
# to each group
@@ -394,11 +357,11 @@ class Discount(LoggedModel):
candidate_groups = [(k, list(g)) for k, g in _groups]
for subevent_id, g in candidate_groups:
benefit_g = [idx for idx in benefit_candidates if positions[idx].subevent_id == subevent_id]
benefit_g = [idx for idx in benefit_candidates if positions[idx][1] == subevent_id]
if self.condition_min_count:
self._apply_min_count(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
self._apply_min_count(positions, g, benefit_g, result)
else:
self._apply_min_value(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
self._apply_min_value(positions, g, benefit_g, result)
elif self.subevent_mode == self.SUBEVENT_MODE_DISTINCT:
if self.condition_min_value or not self.benefit_same_products:
@@ -414,9 +377,9 @@ class Discount(LoggedModel):
# Build a list of subevent IDs in descending order of frequency
subevent_to_idx = defaultdict(list)
for idx, p in positions.items():
subevent_to_idx[p.subevent_id].append(idx)
subevent_to_idx[p[1]].append(idx)
for v in subevent_to_idx.values():
v.sort(key=lambda idx: positions[idx].line_price_gross)
v.sort(key=lambda idx: positions[idx][2])
subevent_order = sorted(list(subevent_to_idx.keys()), key=lambda s: len(subevent_to_idx[s]), reverse=True)
# Build groups of exactly condition_min_count distinct subevents
@@ -431,7 +394,7 @@ class Discount(LoggedModel):
l = [ll for ll in l if ll in condition_candidates and ll not in current_group]
if cardinality and len(l) != cardinality:
continue
if se not in {positions[idx].subevent_id for idx in current_group}:
if se not in {positions[idx][1] for idx in current_group}:
candidates += l
cardinality = len(l)
@@ -440,7 +403,7 @@ class Discount(LoggedModel):
# Sort the list by prices, then pick one. For "buy 2 get 1 free" we apply a "pick 1 from the start
# and 2 from the end" scheme to optimize price distribution among groups
candidates = sorted(candidates, key=lambda idx: positions[idx].line_price_gross)
candidates = sorted(candidates, key=lambda idx: positions[idx][2])
if len(current_group) < (self.benefit_only_apply_to_cheapest_n_matches or 0):
candidate = candidates[0]
else:
@@ -452,14 +415,14 @@ class Discount(LoggedModel):
if len(current_group) >= max(self.condition_min_count, 1):
candidate_groups.append(current_group)
for c in current_group:
subevent_to_idx[positions[c].subevent_id].remove(c)
subevent_to_idx[positions[c][1]].remove(c)
current_group = []
# Distribute "leftovers"
for se in subevent_order:
if subevent_to_idx[se]:
for group in candidate_groups:
if se not in {positions[idx].subevent_id for idx in group}:
if se not in {positions[idx][1] for idx in group}:
group.append(subevent_to_idx[se].pop())
if not subevent_to_idx[se]:
break
@@ -469,8 +432,6 @@ class Discount(LoggedModel):
positions,
[idx for idx in g if idx in condition_candidates],
[idx for idx in g if idx in benefit_candidates],
result,
None,
None
result
)
return result
-5
View File
@@ -870,12 +870,10 @@ class Event(EventMixin, LoggedModel):
for i in Item.objects.filter(event=other).prefetch_related(
'variations', 'limit_sales_channels', 'require_membership_types',
'variations__limit_sales_channels', 'variations__require_membership_types',
'matched_by_cross_selling_categories',
):
vars = list(i.variations.all())
require_membership_types = list(i.require_membership_types.all())
limit_sales_channels = list(i.limit_sales_channels.all())
matched_by_cross_selling_categories = list(i.matched_by_cross_selling_categories.all())
item_map[i.pk] = i
i.pk = None
i.event = self
@@ -913,9 +911,6 @@ class Event(EventMixin, LoggedModel):
if not v.all_sales_channels:
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
if matched_by_cross_selling_categories:
i.matched_by_cross_selling_categories.set([category_map[c.pk] for c in matched_by_cross_selling_categories])
for i in self.items.filter(hidden_if_item_available__isnull=False):
i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
i.save()
+13 -53
View File
@@ -63,13 +63,14 @@ from django_countries.fields import Country
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import Event, SubEvent
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.models.tax import TaxedPrice
from pretix.base.timemachine import time_machine_now
from pretix.helpers.images import ImageSizeValidator
from ...helpers.images import ImageSizeValidator
from ..media import MEDIA_TYPES
from .event import Event, SubEvent
class ItemCategory(LoggedModel):
@@ -110,33 +111,6 @@ class ItemCategory(LoggedModel):
'only be bought in combination with a product that has this category configured as a possible '
'source for add-ons.')
)
CROSS_SELLING_MODES = (
(None, _('Normal category')),
('both', _('Normal + cross-selling category')),
('only', _('Cross-selling category')),
)
cross_selling_mode = models.CharField(
choices=CROSS_SELLING_MODES,
null=True,
max_length=5
)
CROSS_SELLING_CONDITION = (
('always', _('Always show in cross-selling step')),
('discounts', _('Only show products that qualify for a discount according to discount rules')),
('products', _('Only show if the cart contains one of the following products')),
)
cross_selling_condition = models.CharField(
verbose_name=_("Cross-selling condition"),
choices=CROSS_SELLING_CONDITION,
null=True,
max_length=10,
)
cross_selling_match_products = models.ManyToManyField(
'pretixbase.Item',
blank=True,
verbose_name=_("Cross-selling condition products"),
related_name="matched_by_cross_selling_categories",
)
class Meta:
verbose_name = _("Product category")
@@ -145,31 +119,19 @@ class ItemCategory(LoggedModel):
def __str__(self):
name = self.internal_name or self.name
category_type = self.get_category_type_display()
if category_type:
return _('{category} ({category_type})').format(category=str(name), category_type=category_type)
if self.is_addon:
return _('{category} (Add-On products)').format(category=str(name))
return str(name)
def get_category_type_display(self):
if self.is_addon:
return _('Add-on category')
elif self.cross_selling_mode:
return self.get_cross_selling_mode_display()
return _('Add-On products')
else:
return None
@property
def category_type(self):
return 'addon' if self.is_addon else self.cross_selling_mode or 'normal'
@category_type.setter
def category_type(self, new_value):
if new_value == 'addon':
self.is_addon = True
self.cross_selling_mode = None
else:
self.is_addon = False
self.cross_selling_mode = None if new_value == 'normal' else new_value
return 'addon' if self.is_addon else 'normal'
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
@@ -308,7 +270,7 @@ class SubEventItemVariation(models.Model):
return True
def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
# makes the query SIGNIFICANTLY faster
from .organizer import SalesChannel
@@ -329,8 +291,6 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_
if not allow_addons:
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
if not allow_cross_sell:
q &= Q(Q(category__isnull=True) | ~Q(category__cross_selling_mode='only'))
if voucher:
if voucher.item_id:
@@ -344,8 +304,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_
class ItemQuerySet(models.QuerySet):
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
return filter_available(self, channel, voucher, allow_addons, allow_cross_sell)
def filter_available(self, channel='web', voucher=None, allow_addons=False):
return filter_available(self, channel, voucher, allow_addons)
class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__):
@@ -353,8 +313,8 @@ class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__)
super().__init__()
self._queryset_class = ItemQuerySet
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
return filter_available(self.get_queryset(), channel, voucher, allow_addons, allow_cross_sell)
def filter_available(self, channel='web', voucher=None, allow_addons=False):
return filter_available(self.get_queryset(), channel, voucher, allow_addons)
class Item(LoggedModel):
-8
View File
@@ -2835,14 +2835,6 @@ class OrderPosition(AbstractPosition):
(self.order.event.settings.change_allow_user_addons and ItemAddOn.objects.filter(base_item_id__in=[op.item_id for op in positions]).exists())
)
@property
def code(self):
"""
A ticket code which is unique among all events of a single organizer,
built by the order code and the position number.
"""
return '{order_code}-{position}'.format(order_code=self.order.code, position=self.positionid)
class Transaction(models.Model):
"""
+5 -15
View File
@@ -29,8 +29,6 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.deconstruct import deconstructible
from django.utils.formats import localize
from django.utils.functional import lazy
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.fields import I18nCharField
from i18nfield.strings import LazyI18nString
@@ -122,8 +120,6 @@ EU_CURRENCIES = {
}
VAT_ID_COUNTRIES = EU_COUNTRIES | {'CH', 'NO'}
format_html_lazy = lazy(format_html, str)
def is_eu_country(cc):
cc = str(cc)
@@ -197,17 +193,11 @@ class TaxRule(LoggedModel):
eu_reverse_charge = models.BooleanField(
verbose_name=_("Use EU reverse charge taxation rules"),
default=False,
help_text=format_html_lazy(
'<span class="label label-warning" data-toggle="tooltip" title="{}">{}</span> {}',
_('This feature will be removed in the future as it does not handle VAT for non-business customers in '
'other EU countries in a way that works for all organizers. Use custom rules instead.'),
_('DEPRECATED'),
_("Not recommended. Most events will NOT be qualified for reverse charge since the place of "
"taxation is the location of the event. This option disables charging VAT for all customers "
"outside the EU and for business customers in different EU countries who entered a valid EU VAT "
"ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax "
"calculation. USE AT YOUR OWN RISK.")
),
help_text=_("Not recommended. Most events will NOT be qualified for reverse charge since the place of "
"taxation is the location of the event. This option disables charging VAT for all customers "
"outside the EU and for business customers in different EU countries who entered a valid EU VAT "
"ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax "
"calculation. USE AT YOUR OWN RISK.")
)
home_country = FastCountryField(
verbose_name=_('Merchant country'),
+9 -11
View File
@@ -1542,9 +1542,10 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
:param voucher: A voucher code
:param cart_id: The cart ID of the cart to modify
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
@@ -1565,10 +1566,10 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
"""
Removes an item specified by its position ID from a user's cart.
Removes a list of items from a user's cart.
:param event: The event ID in question
:param position: A cart position ID
:param cart_id: The cart ID of the cart to modify
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
@@ -1589,9 +1590,9 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
"""
Removes all items from a user's cart.
Removes a list of items from a user's cart.
:param event: The event ID in question
:param cart_id: The cart ID of the cart to modify
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
try:
@@ -1610,15 +1611,13 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: List[dict], cart_id: str=None, locale='en',
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
"""
Assigns addons to eligible products in a user's cart, adding and removing the addon products as necessary to
ensure the requested addon state.
Removes a list of items from a user's cart.
:param event: The event ID in question
:param addons: A list of dicts with the keys addon_to, item, variation
:param add_to_cart_items: A list of dicts with the keys item, variation, count, custom_price, voucher, seat ID
:param cart_id: The cart ID of the cart to modify
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
ia = False
@@ -1636,7 +1635,6 @@ def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: L
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
cm.set_addons(addons)
cm.add_new_items(add_to_cart_items)
cm.commit()
except LockTimeoutException:
self.retry()
+2 -3
View File
@@ -1182,11 +1182,10 @@ def process_exit_all(sender, **kwargs):
positions = cl.positions_inside_query(ignore_status=True, at_time=cl.exit_all_at)
for p in positions:
with scope(organizer=cl.event.organizer):
ci, created = Checkin.objects.get_or_create(
ci = Checkin.objects.create(
position=p, list=cl, auto_checked_in=True, type=Checkin.TYPE_EXIT, datetime=cl.exit_all_at
)
if created:
checkin_created.send(cl.event, checkin=ci)
checkin_created.send(cl.event, checkin=ci)
d = cl.exit_all_at.astimezone(cl.event.timezone)
if cl.event.settings.get(f'autocheckin_dst_hack_{cl.pk}'): # move time back if yesterday was DST switch
d -= timedelta(hours=1)
-232
View File
@@ -1,232 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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 collections import defaultdict
from decimal import Decimal
from itertools import groupby
from math import inf
from typing import List
from django.utils.functional import cached_property
from pretix.base.models import CartPosition, ItemCategory, SalesChannel
from pretix.presale.views.event import get_grouped_items
class DummyCategory:
"""
Used to create fake category objects for displaying the same cross-selling category multiple times,
once for each subevent
"""
def __init__(self, category: ItemCategory, subevent):
self.id = category.id
self.name = str(category.name)
self.subevent_name = str(subevent)
self.description = category.description
class CrossSellingService:
def __init__(self, event, sales_channel: SalesChannel, cartpositions: List[CartPosition], customer):
self.event = event
self.sales_channel = sales_channel
self.cartpositions = cartpositions
self.customer = customer
def get_data(self):
if self.event.has_subevents:
subevents = set(pos.subevent for pos in self.cartpositions)
result = (
(DummyCategory(category, subevent),
self._prepare_items(subevent, items_qs, discount_info),
f'subevent_{subevent.pk}_')
for subevent in subevents
for (category, items_qs, discount_info) in self._applicable_categories(subevent.pk)
)
else:
result = (
(category,
self._prepare_items(None, items_qs, discount_info),
'')
for (category, items_qs, discount_info) in self._applicable_categories(0)
)
result = [(category, items, form_prefix) for (category, items, form_prefix) in result if len(items) > 0]
for category, items, form_prefix in result:
category.category_has_discount = any(item.original_price for item in items)
return result
def _applicable_categories(self, subevent_id):
return [
(c, products_qs, discount_info) for (c, products_qs, discount_info) in
(
(c, *self._get_visible_items_for_category(subevent_id, c))
for c in self.event.categories.filter(cross_selling_mode__isnull=False).prefetch_related('items')
)
if products_qs is not None
]
def _get_visible_items_for_category(self, filter_subevent_id, category: ItemCategory):
"""
If this category should be visible in the cross-selling step for a given cart and sales_channel, this method
returns a queryset of the items that should be displayed, as well as a dict giving additional information on them.
:returns: (QuerySet<Item>, dict<(subevent_id, item_pk): (max_count, discount_rule)>)
max_count is `inf` if the item should not be limited
discount_rule is None if the item will not be discounted
"""
if category.cross_selling_mode is None:
return None, {}
if category.cross_selling_condition == 'always':
return category.items.all(), {}
if category.cross_selling_condition == 'products':
match = set(match.pk for match in category.cross_selling_match_products.only('pk')) # TODO prefetch this
return (category.items.all(), {}) if any(pos.item.pk in match for pos in self.cartpositions) else (None, {})
if category.cross_selling_condition == 'discounts':
my_item_pks = [item.id for item in category.items.all()]
potential_discount_items = {
item.pk: (max_count, discount_rule)
for subevent_id, item, max_count, discount_rule in self._potential_discounts_by_subevent_and_item_for_current_cart
if max_count > 0 and item.pk in my_item_pks and item.is_available() and (subevent_id == filter_subevent_id or subevent_id is None)
}
return category.items.filter(pk__in=potential_discount_items), potential_discount_items
@cached_property
def _potential_discounts_by_subevent_and_item_for_current_cart(self):
potential_discounts_by_cartpos = defaultdict(list)
from ..services.pricing import apply_discounts
self._discounted_prices = apply_discounts(
self.event,
self.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled,
cp.listed_price - cp.price_after_voucher)
for cp in self.cartpositions
],
collect_potential_discounts=potential_discounts_by_cartpos
)
# flatten potential_discounts_by_cartpos (a dict of lists of potential discounts) into a set of potential discounts
# (which is technically stored as a dict, but we use it as an OrderedSet here)
potential_discount_set = dict.fromkeys(
info for lst in potential_discounts_by_cartpos.values() for info in lst)
# sum up the max_counts and pass them on (also pass on the discount_rules so we can calculate actual discounted prices later):
# group by benefit product
# - max_count for product: sum up max_counts
# - discount_rule for product: take first discount_rule
def discount_info(subevent_id, item, infos_for_item):
infos_for_item = list(infos_for_item)
return (
subevent_id,
item,
sum(max_count for (subevent_id, item, discount_rule, max_count, i) in infos_for_item),
next(discount_rule for (subevent_id, item, discount_rule, max_count, i) in infos_for_item),
)
return [
discount_info(subevent_id, item, infos_for_item) for (subevent_id, item), infos_for_item in
groupby(
sorted(
(
(subevent_id, item, discount_rule, max_count, i)
for (discount_rule, max_count, i, subevent_id) in potential_discount_set.keys()
for item in discount_rule.benefit_limit_products.all()
),
key=lambda tup: (tup[0], tup[1].pk)
),
lambda tup: (tup[0], tup[1]))
]
def _prepare_items(self, subevent, items_qs, discount_info):
items, _btn = get_grouped_items(
self.event,
subevent=subevent,
voucher=None,
channel=self.sales_channel,
base_qs=items_qs,
allow_addons=False,
allow_cross_sell=True,
memberships=(
self.customer.usable_memberships(
for_event=subevent or self.event,
testmode=self.event.testmode
)
if self.customer else None
),
)
new_items = list()
for item in items:
max_count = inf
if item.pk in discount_info:
(max_count, discount_rule) = discount_info[item.pk]
# only benefit_only_apply_to_cheapest_n_matches discounted items have a max_count, all others get 'inf'
if not max_count:
max_count = inf
# calculate discounted price
if discount_rule and discount_rule.benefit_discount_matching_percent > 0:
if not item.has_variations:
item.original_price = item.original_price or item.display_price
previous_price = item.display_price
new_price = (
previous_price * (
(Decimal('100.00') - discount_rule.benefit_discount_matching_percent) / Decimal('100.00'))
)
item.display_price = new_price
else:
# discounts always match "whole" items, not specific variations -> we apply the discount to all
# available variations of the item
for var in item.available_variations:
var.original_price = var.original_price or var.display_price
previous_price = var.display_price
new_price = (
previous_price * (
(Decimal('100.00') - discount_rule.benefit_discount_matching_percent) / Decimal('100.00'))
)
var.display_price = new_price
if not item.has_variations:
# reduce order_max by number of items already in cart (prevent recommending a product the user can't add anyway)
item.order_max = min(
item.order_max - sum(1 for pos in self.cartpositions if pos.item_id == item.pk),
max_count
)
if item.order_max > 0:
new_items.append(item)
else:
new_vars = list()
for var in item.available_variations:
# reduce order_max by number of items already in cart (prevent recommending a product the user can't add anyway)
var.order_max = min(
var.order_max - sum(1 for pos in self.cartpositions if pos.item_id == item.pk and pos.variation_id == var.pk),
max_count
)
if var.order_max > 0:
new_vars.append(var)
if len(new_vars):
item.available_variations = new_vars
new_items.append(item)
return new_items
+5 -15
View File
@@ -20,9 +20,8 @@
# <https://www.gnu.org/licenses/>.
#
import re
from collections import defaultdict
from decimal import Decimal
from typing import List, Optional, Tuple, Union
from typing import List, Optional, Tuple
from django import forms
from django.db.models import Q
@@ -32,7 +31,6 @@ from pretix.base.models import (
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation,
SalesChannel, Voucher,
)
from pretix.base.models.discount import Discount, PositionInfo
from pretix.base.models.event import Event, SubEvent
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.timemachine import time_machine_now
@@ -157,22 +155,14 @@ def get_line_price(price_after_voucher: Decimal, custom_price_input: Decimal, cu
return price
def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
positions: List[Tuple[int, Optional[int], Decimal, bool, bool, Decimal]],
collect_potential_discounts: Optional[defaultdict]=None) -> List[Tuple[Decimal, Optional[Discount]]]:
def apply_discounts(event: Event, sales_channel: str,
positions: List[Tuple[int, Optional[int], Decimal, bool, bool]]) -> List[Decimal]:
"""
Applies any dynamic discounts to a cart
:param event: Event the cart belongs to
:param sales_channel: Sales channel the cart was created with
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
:param collect_potential_discounts: If a `defaultdict(list)` is supplied, all discounts that could be applied to the cart
based on the "consumed" items, but lack matching "benefitting" items will be collected therein.
The dict will contain a mapping from index in the `positions` list of the item that could be consumed, to a list
of tuples describing the discounts that could be applied in the form `(discount, max_count, grouping_id)`.
`max_count` is either the maximum number of benefitting items that the discount would apply to, or `inf` if that number
is not limited. The `grouping_id` can be used to distinguish several occurrences of the same discount.
:return: A list of ``(new_gross_price, discount)`` tuples in the same order as the input
"""
if isinstance(sales_channel, SalesChannel):
@@ -187,10 +177,10 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
for discount in discount_qs:
result = discount.apply({
idx: PositionInfo(item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)
idx: (item_id, subevent_id, line_price_gross, is_addon_to, voucher_discount)
for idx, (item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount) in enumerate(positions)
if not is_bundled and idx not in new_prices
}, collect_potential_discounts)
})
for k in result.keys():
result[k] = (result[k], discount)
new_prices.update(result)
+1 -2
View File
@@ -1295,8 +1295,7 @@ DEFAULTS = {
'form_kwargs': dict(
label=_("Show event times and dates on the ticket shop"),
help_text=_("If disabled, no date or time will be shown on the ticket shop's front page. This settings "
"also affects a few other locations, however it should not be expected that the date of the "
"event is shown nowhere to users."),
"does however not affect the display in other locations."),
)
},
'show_date_to': {
+2 -13
View File
@@ -30,9 +30,7 @@ from celery import states
from celery.result import AsyncResult
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import (
BadRequest, PermissionDenied, ValidationError,
)
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.files.uploadedfile import UploadedFile
from django.db import transaction
from django.http import HttpResponse, JsonResponse, QueryDict
@@ -133,8 +131,6 @@ class AsyncMixin:
return data
def get_result(self, request):
if not request.GET.get('async_id'):
raise BadRequest("No async_id given")
res = AsyncResult(request.GET.get('async_id'))
if 'ajax' in self.request.GET:
return JsonResponse(self._return_ajax_result(res, timeout=0.25))
@@ -144,12 +140,7 @@ class AsyncMixin:
return self.success(res.info)
else:
return self.error(res.info)
state, info = res.state, res.info
return render(request, 'pretixpresale/waiting.html', {
'started': state in ('PROGRESS', 'STARTED'),
'percentage': info.get('value', 0) if isinstance(info, dict) else 0,
'steps': info.get('steps', []) if isinstance(info, dict) else None,
})
return render(request, 'pretixpresale/waiting.html')
def success(self, value):
smes = self.get_success_message(value)
@@ -217,8 +208,6 @@ class AsyncAction(AsyncMixin):
def get(self, request, *args, **kwargs):
if 'async_id' in request.GET and settings.HAS_CELERY:
if not request.GET.get('async_id'):
raise BadRequest("No async_id given")
return self.get_result(request)
return self.http_method_not_allowed(request)
+2 -2
View File
@@ -1143,12 +1143,12 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
widget=I18nTextInput,
)
mail_subject_order_incomplete_payment = I18nFormField(
label=_("Subject (if an incomplete payment was received)"),
label=_("Subject"),
required=False,
widget=I18nTextInput,
)
mail_text_order_incomplete_payment = I18nFormField(
label=_("Text (if an incomplete payment was received)"),
label=_("Text"),
required=False,
widget=I18nMarkdownTextarea,
help_text=_("This email only applies to payment methods that can receive incomplete payments, "
+2 -59
View File
@@ -40,8 +40,7 @@ from urllib.parse import urlencode
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import Max, Q
from django.forms import ChoiceField, RadioSelect
from django.db.models import Max
from django.forms.formsets import DELETION_FIELD_NAME
from django.urls import reverse
from django.utils.functional import cached_property
@@ -80,67 +79,11 @@ class CategoryForm(I18nModelForm):
'name',
'internal_name',
'description',
'cross_selling_condition',
'cross_selling_match_products',
'is_addon'
]
widgets = {
'description': I18nMarkdownTextarea,
'cross_selling_condition': RadioSelect,
}
field_classes = {
'cross_selling_match_products': SafeModelMultipleChoiceField,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tpl = '{} &nbsp; <span class="text-muted">{}</span>'
self.fields['category_type'] = ChoiceField(widget=RadioSelect, choices=(
('normal', mark_safe(tpl.format(
_('Normal category'),
_('Products in this category are regular products displayed on the front page.')
)),),
('addon', mark_safe(tpl.format(
_('Add-on product category'),
_('Products in this category are add-on products and can only be bought as add-ons.')
)),),
('only', mark_safe(tpl.format(
_('Cross-selling category'),
_('Products in this category are regular products, but are only shown in the cross-selling step, '
'according to the configuration below.')
)),),
('both', mark_safe(tpl.format(
_('Normal + cross-selling category'),
_('Products in this category are regular products displayed on the front page, but are additionally '
'shown in the cross-selling step, according to the configuration below.')
)),),
))
self.fields['category_type'].initial = self.instance.category_type
self.fields['cross_selling_condition'].widget.attrs['data-display-dependency'] = '#id_category_type_2,#id_category_type_3'
self.fields['cross_selling_condition'].widget.attrs['data-disable-dependent'] = 'true'
self.fields['cross_selling_condition'].widget.choices = self.fields['cross_selling_condition'].widget.choices[1:]
self.fields['cross_selling_condition'].required = False
self.fields['cross_selling_match_products'].widget = forms.CheckboxSelectMultiple(
attrs={
'class': 'scrolling-multiple-choice',
'data-display-dependency': '#id_cross_selling_condition_2'
}
)
self.fields['cross_selling_match_products'].queryset = self.event.items.filter(
# don't show products which are only visible in addon/cross-sell step themselves
Q(category__isnull=True) | Q(
Q(category__is_addon=False) & Q(Q(category__cross_selling_mode='both') | Q(category__cross_selling_mode__isnull=True))
)
)
def clean(self):
d = super().clean()
if d.get('category_type') == 'only' or d.get('category_type') == 'both':
if not d.get('cross_selling_condition'):
raise ValidationError({'cross_selling_condition': [_('This field is required')]})
self.instance.category_type = d.get('category_type')
return d
class QuestionForm(I18nModelForm):
@@ -41,7 +41,7 @@
{% bootstrap_field form.eu_reverse_charge layout="control" %}
{% bootstrap_field form.home_country layout="control" %}
{% bootstrap_field form.keep_gross_if_rate_changes layout="control" %}
<h3>{% trans "Custom rules" %}</h3>
<h3>{% trans "Custom taxation rules" %}</h3>
<div class="alert alert-warning">
{% blocktrans trimmed %}
These settings are intended for professional users with very specific taxation situations.
@@ -31,7 +31,6 @@
<thead>
<tr>
<th>{% trans "Product categories" %}</th>
<th>{% trans "Category type" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
@@ -41,9 +40,6 @@
<td>
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
</td>
<td>
{{ c.get_category_type_display }}
</td>
<td class="text-right flip">
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
@@ -16,9 +16,7 @@
{% bootstrap_field form.internal_name layout="control" %}
</div>
{% bootstrap_field form.description layout="control" %}
{% bootstrap_field form.category_type layout="control" horizontal_field_class="big-radio-wrapper col-lg-9" %}
{% bootstrap_field form.cross_selling_condition layout="control" horizontal_field_class="col-lg-9" %}
{% bootstrap_field form.cross_selling_match_products layout="control" %}
{% bootstrap_field form.is_addon layout="control" %}
</fieldset>
</div>
{% if category %}
@@ -133,7 +133,7 @@
{% endif %}
{{ i.default_price|money:request.event.currency }}
{% if i.original_price %}<strike class="text-muted">{{ i.original_price|money:request.event.currency }}</strike>{% endif %}
{% if i.tax_rule %}
{% if i.tax_rule and i.default_price %}
<br/>
<small class="text-muted">
{% if not i.tax_rule.price_includes_tax %}
@@ -102,24 +102,16 @@
</tr>
</thead>
<tbody>
{% for t in tokens %}
{% for t in team.active_tokens %}
<tr>
<td {% if not t.active %}class="text-muted"{% endif %}>
{% if not t.active %}
<del>
{% endif %}
<td>
{{ t.name }}
{% if not t.active %}
</del>
{% endif %}
</td>
<td class="text-right flip">
{% if t.active %}
<button type="submit" name="remove-token" value="{{ t.id }}"
class="btn btn-danger btn-sm btn-block">
<i class="fa fa-times"></i> {% trans "Remove" %}
</button>
{% endif %}
<button type="submit" name="remove-token" value="{{ t.id }}"
class="btn btn-danger btn-sm btn-block">
<i class="fa fa-times"></i> {% trans "Remove" %}
</button>
</td>
</tr>
{% endfor %}
+7 -18
View File
@@ -686,24 +686,14 @@ class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
try:
self.object.log_action('pretix.team.deleted', user=self.request.user)
self.object.delete()
except ProtectedError as e:
is_logs = any(isinstance(e, LogEntry) for e in e.protected_objects)
if is_logs:
messages.error(
self.request,
_(
"The team could not be deleted because the team or one of its API tokens is part of "
"historical audit logs."
)
)
else:
messages.error(
self.request,
_(
'The team could not be deleted as some constraints (e.g. data created by '
'plug-ins) do not allow it.'
)
except ProtectedError:
messages.error(
self.request,
_(
'The team could not be deleted as some constraints (e.g. data created by '
'plug-ins) do not allow it.'
)
)
return redirect(success_url)
messages.success(request, _('The selected team has been deleted.'))
@@ -733,7 +723,6 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
ctx = super().get_context_data(**kwargs)
ctx['add_form'] = self.add_form
ctx['add_token_form'] = self.add_token_form
ctx['tokens'] = self.object.tokens.order_by("-active", "name", "pk")
return ctx
def _send_invite(self, instance):
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"PO-Revision-Date: 2024-09-10 07:17+0000\n"
"PO-Revision-Date: 2024-08-27 16:02+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
"de/>\n"
@@ -572,8 +572,10 @@ msgid "Group of objects"
msgstr "Gruppe von Objekten"
#: pretix/static/pretixcontrol/js/ui/editor.js:899
#, fuzzy
#| msgid "Text object"
msgid "Text object (deprecated)"
msgstr "Text-Objekt (veraltet)"
msgstr "Text-Objekt"
#: pretix/static/pretixcontrol/js/ui/editor.js:901
msgid "Text box"
-3
View File
@@ -4,7 +4,6 @@ AES
Absenderadresse
Absenderinformation
Absendername
ABGEKÜNDIGT
Admin
Adminbereich
Affirm
@@ -26,7 +25,6 @@ Ausgangsscans
ausgeklappt
ausgecheckt
auswahl
Auth
Authentication
Authenticator
Authentifizierungsmechanismus
@@ -38,7 +36,6 @@ Bancontact
BankID
Banking
barcodes
Baskisch
Bcc
BCC
Beispielevent
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"PO-Revision-Date: 2024-09-10 07:17+0000\n"
"PO-Revision-Date: 2024-08-27 16:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/de_Informal/>\n"
@@ -571,8 +571,10 @@ msgid "Group of objects"
msgstr "Gruppe von Objekten"
#: pretix/static/pretixcontrol/js/ui/editor.js:899
#, fuzzy
#| msgid "Text object"
msgid "Text object (deprecated)"
msgstr "Text-Objekt (veraltet)"
msgstr "Text-Objekt"
#: pretix/static/pretixcontrol/js/ui/editor.js:901
msgid "Text box"
@@ -4,7 +4,6 @@ AES
Absenderadresse
Absenderinformation
Absendername
ABGEKÜNDIGT
Admin
Adminbereich
Affirm
@@ -26,7 +25,6 @@ Ausgangsscans
ausgeklappt
ausgecheckt
auswahl
Auth
Authentication
Authenticator
Authentifizierungsmechanismus
@@ -38,7 +36,6 @@ Bancontact
BankID
Banking
barcodes
Baskisch
Bcc
BCC
Beispielevent
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-26 11:23+0000\n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+34 -8
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-26 11:23+0000\n"
"PO-Revision-Date: 2024-09-06 08:47+0000\n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"PO-Revision-Date: 2024-08-26 15:00+0000\n"
"Last-Translator: Albizuri <oier@puntu.eus>\n"
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix-js/"
"eu/>\n"
@@ -557,14 +557,17 @@ msgid "absent"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:171
#: pretix/static/pretixcontrol/js/ui/editor.js:72
msgid "Check-in QR"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:543
#: pretix/static/pretixcontrol/js/ui/editor.js:387
msgid "The PDF background file could not be loaded for the following reason:"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:894
#: pretix/static/pretixcontrol/js/ui/editor.js:656
msgid "Group of objects"
msgstr ""
@@ -577,35 +580,44 @@ msgid "Text box"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:903
#: pretix/static/pretixcontrol/js/ui/editor.js:663
msgid "Barcode area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:905
#: pretix/static/pretixcontrol/js/ui/editor.js:665
msgid "Image area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:907
#: pretix/static/pretixcontrol/js/ui/editor.js:667
msgid "Powered by pretix"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:909
#: pretix/static/pretixcontrol/js/ui/editor.js:669
msgid "Object"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:913
#: pretix/static/pretixcontrol/js/ui/editor.js:673
msgid "Ticket design"
msgstr "Sarrera diseinua"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:1250
#: pretix/static/pretixcontrol/js/ui/editor.js:972
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:1319
#: pretix/static/pretixcontrol/js/ui/editor.js:1370
#: pretix/static/pretixcontrol/js/ui/editor.js:1041
#: pretix/static/pretixcontrol/js/ui/editor.js:1091
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:1353
#: pretix/static/pretixcontrol/js/ui/editor.js:1074
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
@@ -622,14 +634,17 @@ msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:322
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:326
#: pretix/static/pretixcontrol/js/ui/main.js:321
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
@@ -637,38 +652,48 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:491
#: pretix/static/pretixcontrol/js/ui/main.js:511
#: pretix/static/pretixcontrol/js/ui/main.js:475
#: pretix/static/pretixcontrol/js/ui/main.js:495
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:509
#: pretix/static/pretixcontrol/js/ui/main.js:493
msgid "All"
msgstr "Guztiak"
#: pretix/static/pretixcontrol/js/ui/main.js:510
#: pretix/static/pretixcontrol/js/ui/main.js:494
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:514
#: pretix/static/pretixcontrol/js/ui/main.js:498
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:847
#: pretix/static/pretixcontrol/js/ui/main.js:828
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:850
#: pretix/static/pretixcontrol/js/ui/main.js:831
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1008
#: pretix/static/pretixcontrol/js/ui/main.js:989
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1048
#: pretix/static/pretixcontrol/js/ui/main.js:1029
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1123
#: pretix/static/pretixcontrol/js/ui/main.js:1104
msgid "You have unsaved changes!"
msgstr ""
@@ -695,19 +720,16 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Zure saskiko produktuak ez daude zuretzat erreserbatuta. Oraindik ere zure "
"eskaera bete dezakezu, baldin eta eskuragarri badaude."
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "Saskia iraungita"
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:50
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Zure saskiko produktuak minutu -ez erreserbatuta daude zuretzat."
msgstr[0] ""
msgstr[1] ""
"Zure saskiko produktuak {num} minutuz erreserbatuta daude zuretzat."
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
@@ -1075,3 +1097,7 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:89
msgid "December"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:661
msgid "Text object"
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8 -4
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"PO-Revision-Date: 2024-08-28 10:03+0000\n"
"PO-Revision-Date: 2024-06-27 17:00+0000\n"
"Last-Translator: Anarion Dunedain <anarion80@gmail.com>\n"
"Language-Team: Polish <https://translate.pretix.eu/projects/pretix/pretix-js/"
"pl/>\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 5.7\n"
"X-Generator: Weblate 5.6.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -572,12 +572,16 @@ msgid "Group of objects"
msgstr "Grupa obiektów"
#: pretix/static/pretixcontrol/js/ui/editor.js:899
#, fuzzy
#| msgid "Text object"
msgid "Text object (deprecated)"
msgstr "Obiekt tekstowy (przestarzały)"
msgstr "Obiekt tekstowy"
#: pretix/static/pretixcontrol/js/ui/editor.js:901
#, fuzzy
#| msgid "Text object"
msgid "Text box"
msgstr "Pole tekstowe"
msgstr "Obiekt tekstowy"
#: pretix/static/pretixcontrol/js/ui/editor.js:903
msgid "Barcode area"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"PO-Revision-Date: 2024-09-08 11:04+0000\n"
"Last-Translator: Martin Gross <gross@rami.io>\n"
"PO-Revision-Date: 2024-06-08 18:00+0000\n"
"Last-Translator: David Vaz <davidmgvaz@gmail.com>\n"
"Language-Team: Portuguese (Portugal) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/pt_PT/>\n"
"Language: pt_PT\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.7\n"
"X-Generator: Weblate 5.5.5\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -292,8 +292,10 @@ msgid "Information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
#, fuzzy
#| msgid "Unknown error."
msgid "Unknown ticket"
msgstr "Bilhete desconhecido"
msgstr "Erro desconhecido."
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Ticket type not allowed here"
@@ -803,19 +805,21 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "Select"
msgstr "Selecionar"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
#, javascript-format
msgctxt "widget"
msgid "Select %s"
msgstr "Selecionados %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
#, javascript-format
#, fuzzy, javascript-format
#| msgctxt "widget"
#| msgid "See variations"
msgctxt "widget"
msgid "Select variant %s"
msgstr "Selecione variantes %s"
msgstr "Ver alternativas"
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+59 -75
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"PO-Revision-Date: 2024-09-15 18:00+0000\n"
"PO-Revision-Date: 2024-06-26 22:00+0000\n"
"Last-Translator: Kristian Feldsam <feldsam@gmail.com>\n"
"Language-Team: Slovak <https://translate.pretix.eu/projects/pretix/pretix-js/"
"sk/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Weblate 5.7\n"
"X-Generator: Weblate 5.6.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -156,7 +156,7 @@ msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Paid orders"
msgstr "Zaplatené objednávky"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
msgid "Total revenue"
@@ -176,15 +176,15 @@ msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:30
msgid "Select a check-in list"
msgstr "Vyberte zoznam na odbavenie"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31
msgid "No active check-in lists found."
msgstr "Nenašli sa žiadne aktívne zoznamy na odbavenie."
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32
msgid "Switch check-in list"
msgstr "Prepnúť zoznam odbavení"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33
msgid "Search results"
@@ -274,7 +274,7 @@ msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Ticket already used"
msgstr "Už použitá vstupenka"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Information required"
@@ -282,7 +282,7 @@ msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Unknown ticket"
msgstr "Neznáma vstupenka"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Ticket type not allowed here"
@@ -318,7 +318,7 @@ msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets"
msgstr "Odbavené vstupenky"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets"
@@ -398,9 +398,6 @@ msgid ""
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
"Momentálne odosielame vašu požiadavku na server. Ak to trvá dlhšie ako jednu "
"minútu, skontrolujte svoje internetové pripojenie a potom znovu načítajte "
"túto stránku a skúste to znova."
#: pretix/static/pretixbase/js/asynctask.js:301
#: pretix/static/pretixcontrol/js/ui/main.js:71
@@ -549,7 +546,7 @@ msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:171
msgid "Check-in QR"
msgstr "QR na odbavenie"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:543
msgid "The PDF background file could not be loaded for the following reason:"
@@ -686,8 +683,6 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Vstupenky v košíku už nie sú pre Vás rezervované. Objednávku môžete "
"dokončiť, ak sú stále k dispozícii."
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
@@ -696,9 +691,8 @@ msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:50
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Vstupenky v košíku sú pre Vás rezervované jednu minútu."
msgstr[1] "Vstupenky v košíku sú pre Vás rezervované {num} minúty."
msgstr[2] "Vstupenky v košíku sú pre Vás rezervované {num} minút."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
@@ -710,11 +704,11 @@ msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:227
msgid "Please enter the amount the organizer can keep."
msgstr "Zadajte sumu, ktorú si organizátor môže ponechať."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:449
msgid "Please enter a quantity for one of the ticket types."
msgstr "Vyberte si prosím aspoň jednu vstupenku."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:485
msgid "required"
@@ -736,131 +730,131 @@ msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Quantity"
msgstr "Počet"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Decrease quantity"
msgstr "Znížiť počet"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "Increase quantity"
msgstr "Zvýšiť počet"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "Price"
msgstr "Cena"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "Select"
msgstr "Vybrať"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
#, javascript-format
msgctxt "widget"
msgid "Select %s"
msgstr "Vybrať %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
#, javascript-format
msgctxt "widget"
msgid "Select variant %s"
msgstr "Vybrať variantu %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "Sold out"
msgstr "Vypredané"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
msgctxt "widget"
msgid "Buy"
msgstr "Kúpiť"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "Register"
msgstr "Registrovať sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
msgctxt "widget"
msgid "Reserved"
msgstr "Rezervované"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "FREE"
msgstr "ZADARMO"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr "z %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr "vrátane %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid "incl. taxes"
msgstr "vrátane dane"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
msgctxt "widget"
msgid "plus taxes"
msgstr "plus daň"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:33
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr "aktuálne k dispozícii: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid "Only available with a voucher"
msgstr "K dispozícii len s poukážkou"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
msgctxt "widget"
msgid "Not yet available"
msgstr "Zatiaľ nedostupné"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "Not available anymore"
msgstr "Už nie je k dispozícii"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "Currently not available"
msgstr "Momentálne nie je k dispozícii"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr "minimálna suma na objednávku: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Close ticket shop"
msgstr "Zatvoriť obchod so vstupenkami"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:40
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr "Obchod so vstupenkami sa nepodarilo načítať."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
@@ -868,18 +862,16 @@ msgid ""
"There are currently a lot of users in this ticket shop. Please open the shop "
"in a new tab to continue."
msgstr ""
"V súčasnosti je v tomto obchode so vstupenkami veľa používateľov. Ak chcete "
"pokračovať, otvorte obchod v novej karte."
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Open ticket shop"
msgstr "Otvoriť obchod so vstupenkami"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr "Nákupný košík sa nepodarilo vytvoriť. Skúste to prosím neskôr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
@@ -887,14 +879,11 @@ msgid ""
"We could not create your cart, since there are currently too many users in "
"this ticket shop. Please click \"Continue\" to retry in a new tab."
msgstr ""
"Nepodarilo sa nám vytvoriť Váš nákupný košík, pretože v tomto obchode je "
"momentálne príliš veľa používateľov. Kliknutím na tlačidlo „Pokračovať“ to "
"skúste znova v novej karte."
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgctxt "widget"
msgid "Waiting list"
msgstr "Čakací zoznam"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgctxt "widget"
@@ -902,88 +891,86 @@ msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
"V súčasnosti máte aktívny nákupný košík pre toto podujatie. Ak si vyberiete "
"ďalšie vstupenky, pridajú sa do vášho existujúceho košíka."
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgctxt "widget"
msgid "Resume checkout"
msgstr "Pokračovať v objednávke"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgctxt "widget"
msgid "Redeem a voucher"
msgstr "Uplatnenie poukážky"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgctxt "widget"
msgid "Redeem"
msgstr "Uplatniť"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgctxt "widget"
msgid "Voucher code"
msgstr "Kód poukážky"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgctxt "widget"
msgid "Close"
msgstr "Zatvoriť"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
msgctxt "widget"
msgid "Continue"
msgstr "Pokračovať"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:56
msgctxt "widget"
msgid "Show variants"
msgstr "Zobraziť varianty"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:57
msgctxt "widget"
msgid "Hide variants"
msgstr "Skryť varianty"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgctxt "widget"
msgid "Choose a different event"
msgstr "Vybrať iné podujatie"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgctxt "widget"
msgid "Choose a different date"
msgstr "Vybrať iný dátum"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgctxt "widget"
msgid "Back"
msgstr "Späť"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgctxt "widget"
msgid "Next month"
msgstr "Nasledujúci mesiac"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgctxt "widget"
msgid "Previous month"
msgstr "Predchádzajúci mesiac"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgctxt "widget"
msgid "Next week"
msgstr "Nasledujúci týždeň"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgctxt "widget"
msgid "Previous week"
msgstr "Predchádzajúci týždeň"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:65
msgctxt "widget"
msgid "Open seat selection"
msgstr "Zobraziť výber sedadiel"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:66
msgctxt "widget"
@@ -992,14 +979,11 @@ msgid ""
"add yourself to the waiting list. We will then notify if seats are available "
"again."
msgstr ""
"Niektoré alebo všetky kategórie vstupeniek sú v súčasnosti vypredané. Ak "
"chcete, môžete sa pridať na zoznam čakateľov. Budeme vás informovať, ak sa "
"miesta uvoľnia."
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgctxt "widget"
msgid "Load more"
msgstr "Načítať viac"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:69
msgid "Mo"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16 -15
View File
@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-27 13:34+0000\n"
"PO-Revision-Date: 2024-09-12 03:00+0000\n"
"Last-Translator: Tinna Sandström <tinna@coeo.events>\n"
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/"
"pretix-js/sv/>\n"
"PO-Revision-Date: 2024-06-27 17:00+0000\n"
"Last-Translator: Erik Löfman <erik@disruptiveventures.se>\n"
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/sv/>\n"
"Language: sv\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.7\n"
"X-Generator: Weblate 5.6.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -727,8 +727,8 @@ msgstr "Varukorgen har gått ut"
#: pretix/static/pretixpresale/js/ui/cart.js:50
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Produkterna i din bokning är reserverade för dig i en minut."
msgstr[1] "Produkterna i din bokning är reserverade för dig i {num} minuter."
msgstr[0] "Artiklarna i din varukorg är reserverade för dig i en minut."
msgstr[1] "Artiklarna i din varukorg är reserverade för dig i {num} minuter."
#: pretix/static/pretixpresale/js/ui/main.js:203
msgid "The organizer keeps %(currency)s %(amount)s"
@@ -819,7 +819,7 @@ msgstr "Köp"
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "Register"
msgstr "BOKA"
msgstr "Registrera"
#: pretix/static/pretixpresale/js/widget/widget.js:26
msgctxt "widget"
@@ -829,7 +829,7 @@ msgstr "Reserverad"
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "FREE"
msgstr "ANTAL"
msgstr "GRATIS"
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
@@ -921,7 +921,7 @@ msgstr "Öppna biljettbutik"
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr "Bokningen kunde inte skapas. Vänligen försök senare."
msgstr "Varukorgen kunde inte skapas. Vänligen försök senare"
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
@@ -929,8 +929,9 @@ msgid ""
"We could not create your cart, since there are currently too many users in "
"this ticket shop. Please click \"Continue\" to retry in a new tab."
msgstr ""
"Vi kunde inte skapa din bokning, då det just nu är många användare i den här "
"biljettbutiken. Klicka på \"Fortsätt\" för att försöka på nytt i en ny flik."
"Vi kunde inte skapa din varukorg, då det just nu är många användare i den "
"här biljettbutiken. Klicka på \"Fortsätt\" för att försöka på nytt i en ny "
"flik."
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgctxt "widget"
@@ -943,13 +944,13 @@ msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
"Du har för tillfället en pågående bokning för den här eventet. Om du väljer "
"fler produkter, kommer de att läggas till din befintliga bokning."
"Du har för tillfället en aktiv varukorg för den här eventet. Om du väljer "
"fler artiklar, kommer de att läggas till din befintliga varukorg."
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgctxt "widget"
msgid "Resume checkout"
msgstr "Fortsätt med ditt bokningen"
msgstr "Fortsätt med ditt köp"
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgctxt "widget"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1
View File
@@ -9,7 +9,6 @@ acceptor
analytics
anonymize
anonymized
Auth
authenticator
automatical
availabilities
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+15 -15
View File
@@ -162,21 +162,6 @@ def _handle_transaction(trans: BankTransaction, matches: tuple, event: Event = N
else:
trans.order = orders[0]
if len(orders) > 1:
# Multi-match! Can we split this automatically?
order_pending_sum = sum(o.pending_sum for o in orders)
if order_pending_sum != trans.amount:
# we can't :( this needs to be dealt with by a human
trans.state = BankTransaction.STATE_NOMATCH
trans.message = gettext_noop('Automatic split to multiple orders not possible.')
trans.save()
return
# we can!
splits = [(o, o.pending_sum) for o in orders]
else:
splits = [(orders[0], trans.amount)]
for o in orders:
if o.status == Order.STATUS_PAID and o.pending_sum <= Decimal('0.00'):
trans.state = BankTransaction.STATE_DUPLICATE
@@ -194,6 +179,21 @@ def _handle_transaction(trans: BankTransaction, matches: tuple, event: Event = N
trans.save()
return
if len(orders) > 1:
# Multi-match! Can we split this automatically?
order_pending_sum = sum(o.pending_sum for o in orders)
if order_pending_sum != trans.amount:
# we can't :( this needs to be dealt with by a human
trans.state = BankTransaction.STATE_NOMATCH
trans.message = gettext_noop('Automatic split to multiple orders not possible.')
trans.save()
return
# we can!
splits = [(o, o.pending_sum) for o in orders]
else:
splits = [(orders[0], trans.amount)]
trans.state = BankTransaction.STATE_VALID
for order, amount in splits:
info_data = {

Some files were not shown because too many files have changed in this diff Show More