Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
e2dad8fdcc Database consistency checks 2021-10-29 12:02:00 +02:00
187 changed files with 37822 additions and 52436 deletions

View File

@@ -1,15 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip"
directory: "/src"
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/src/pretix/static/npm_dir"
schedule:
interval: "monthly"

View File

@@ -26,13 +26,6 @@ In addition to these standard update steps, the following list issues steps that
to specific versions for pretix. If you're skipping versions, please read the instructions for every version in
between as well.
Upgrade to 3.17.0 or newer
""""""""""""""""""""""""""
pretix 3.17 introduces a dependency on ``nodejs``, so you should install it on your system::
# apt install nodejs npm
Upgrade to 4.4.0 or newer
"""""""""""""""""""""""""

View File

@@ -31,6 +31,5 @@ Resources and endpoints
webhooks
seatingplans
exporters
sendmail_rules
billing_invoices
billing_var

View File

@@ -78,12 +78,6 @@ lines list of objects The actual invo
an event series not created by a product (e.g. shipping or
cancellation fees) as well as whenever the respective (sub)event
has no end date set.
├ event_location string Location of the (sub)event this line was created for as it
was set during invoice creation. Can be ``null`` for all invoice
lines created before this was introduced as well as for lines in
an event series not created by a product (e.g. shipping or
cancellation fees) as well as whenever the respective (sub)event
has no location set.
├ attendee_name string Attendee name at time of invoice creation. Can be ``null`` if no
name was set or if names are configured to not be added to invoices.
├ gross_value money (string) Price including taxes
@@ -116,10 +110,6 @@ internal_reference string Customer's refe
The attributes ``fee_type`` and ``fee_internal_type`` have been added.
.. versionchanged:: 4.1
The attribute ``lines.event_location`` has been added.
Endpoints
---------
@@ -189,7 +179,6 @@ Endpoints
"fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"event_location": "Heidelberg",
"attendee_name": null,
"gross_value": "23.00",
"tax_value": "0.00",
@@ -278,7 +267,6 @@ Endpoints
"fee_internal_type": null,
"event_date_from": "2017-12-27T10:00:00Z",
"event_date_to": null,
"event_location": "Heidelberg",
"attendee_name": null,
"gross_value": "23.00",
"tax_value": "0.00",

View File

@@ -1,281 +0,0 @@
Automated email rules
=====================
Resource description
--------------------
Automated email rules that specify emails that the system will send automatically at a specific point in time, e.g.
the day of the event.
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the rule
enabled boolean If ``false``, the rule is ignored
subject multi-lingual string The subject of the email
template multi-lingual string The body of the email
all_products boolean If ``true``, the email is sent to buyers of all products
limit_products list of integers List of product IDs, if ``all_products`` is not set
include_pending boolean If ``true``, the email is sent to pending orders. If ``false``,
only paid orders are considered.
date_is_absolute boolean If ``true``, the email is set at a specific point in time.
send_date datetime If ``date_is_absolute`` is set: Date and time to send the email.
send_offset_days integer If ``date_is_absolute`` is not set, this is the number of days
before/after the email is sent.
send_offset_time time If ``date_is_absolute`` is not set, this is the time of day the
email is sent on the day specified by ``send_offset_days``.
offset_to_event_end boolean If ``true``, ``send_offset_days`` is relative to the event end
date. Otherwise it is relative to the event start date.
offset_is_after boolean If ``true``, ``send_offset_days`` is the number of days **after**
the event start or end date. Otherwise it is the number of days
**before**.
send_to string Can be ``"orders"`` if the email should be sent to customers
(one email per order),
``"attendees"`` if the email should be sent to every attendee,
or ``"both"``.
date. Otherwise it is relative to the event start date.
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/
Returns a list of all rules configured for an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"enabled": true,
"subject": {"en": "See you tomorrow!"},
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
"date_is_absolute": false,
"offset_to_event_end": false,
"offset_is_after": false,
"send_to": "orders"
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/(id)/
Returns information on one rule, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"enabled": true,
"subject": {"en": "See you tomorrow!"},
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
"date_is_absolute": false,
"offset_to_event_end": false,
"offset_is_after": false,
"send_to": "orders"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the rule to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/
Create a new rule.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 166
{
"enabled": true,
"subject": {"en": "See you tomorrow!"},
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
"date_is_absolute": false,
"offset_to_event_end": false,
"offset_is_after": false,
"send_to": "orders"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"enabled": true,
"subject": {"en": "See you tomorrow!"},
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
"date_is_absolute": false,
"offset_to_event_end": false,
"offset_is_after": false,
"send_to": "orders"
}
:param organizer: The ``slug`` field of the organizer to create a rule for
:param event: The ``slug`` field of the event to create a rule for
:statuscode 201: no error
:statuscode 400: The rule could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create rules.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/(id)/
Update a rule. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 34
{
"enabled": false,
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 1,
"enabled": false,
"subject": {"en": "See you tomorrow!"},
"template": {"en": "Don't forget your tickets, download them at {url}"},
"all_products": true,
"limit_products": [],
"include_pending": false,
"send_date": null,
"send_offset_days": 1,
"send_offset_time": "18:00",
"date_is_absolute": false,
"offset_to_event_end": false,
"offset_is_after": false,
"send_to": "orders"
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the rule to modify
:statuscode 200: no error
:statuscode 400: The rule could not be modified due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/sendmail_rules/(id)/
Delete a rule.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/sendmail_rules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the rule to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this rule cannot be deleted since it is currently in use.

View File

@@ -1,11 +1,6 @@
.. spelling:: Rebase rebasing
Coding style and quality
========================
Code
----
* Basically, we want all python code to follow the `PEP 8`_ standard. There are a few exceptions where
we see things differently or just aren't that strict. The ``setup.cfg`` file in the project's source
folder contains definitions that allow `flake8`_ to check for violations automatically. See :ref:`checksandtests`
@@ -25,62 +20,8 @@ Code
test suite are in the style of Python's unit test module. If you extend those files, you might continue in this style,
but please use ``pytest`` style for any new test files.
Commits and Pull Requests
-------------------------
Most commits should start as pull requests, therefore this applies to the titles of pull requests as well since
the pull request title will become the commit message on merge. We prefer merging with GitHub's "Squash and merge"
feature if the PR contains multiple commits that do not carry value to keep. If there is value in keeping the
individual commits, we use "Rebase and merge" instead. Merge commits should be avoided.
* The commit message should start with a single subject line and can optionally be followed by a commit message body.
* The subject line should be the shortest possible representation of what the commit changes. Someone who reviewed
the commit should able to immediately remember the commit in a couple of weeks based on the subject line and tell
it apart from other commits.
* If there's additional useful information that we should keep, such as reasoning behind the commit, you can
add a longer body, separated from the first line by a blank line.
* The body should explain **what** you changed and more importantly **why** you changed it. There's no need to iterate
**how** you changed something.
* The subject line should be capitalized ("Add new feature" instead of "add new feature") and should not end with a period
("Add new feature" instead of "Add new feature.")
* The subject line should be written in imperative mood, as if you were giving a command what the computer should do if the
commit is applied. This is how generated commit messages by git itself are already written ("Merge branch …", "Revert …")
and makes for short and consistent messages.
* Good: "Fix typo in template"
* Good: "Add Chinese translation"
* Good: "Remove deprecated method"
* Good: "Bump version to 4.4.0"
* Bad: "Fixed bug with …"
* Bad: "Fixes bug with …"
* Bad: "Fixing bug …"
* If all changes in your commit are in context of a single feature or e.g. a bundled plugin, it makes sense to prefix the
subject line with the name of that feature. Examples:
* "API: Add support for PATCH on customers"
* "Docs: Add chapter on alpaca feeding"
* "Stripe: Fix duplicate payments"
* "Order change form: Fix incorrect validation"
* If your commit references a GitHub issue that is fully resolved by your commit, start your subject line with the issue
ID in the form of "Fix #1234 -- Crash in order list". In this case, you can omit the verb "Fix" at the beginning of the
second part of the message to avoid repetition of the word "fix". If your commit only partially resolves the issue, use
"Refs #1234 -- Crash in order list" instead.
* Applies to pretix employees only: If your commit references a sentry issue, please put it in parentheses at the end
of the subject line or inside the body ("Fix crash in order list (PRETIXEU-ABC)"). If your commit references a support
ticket, please put it in parentheses at the end of the subject line with a "Z#" prefix ("Fix crash in order list (Z#12345)").
* If your PR was open for a while and might cause conflicts on merge, please prefer rebasing it (``git rebase -i master``)
over merging ``master`` into your branch unless it is prohibitively complicated.
* Please keep the first line of your commit messages short. When referencing an issue, please phrase it like
``Fix #123 -- Problems with order creation`` or ``Refs #123 -- Fix this part of that bug``.
.. _PEP 8: https://legacy.python.org/dev/peps/pep-0008/

View File

@@ -26,7 +26,7 @@ Your should install the following on your system:
* ``libssl`` (Debian package: ``libssl-dev``)
* ``libxml2`` (Debian package ``libxml2-dev``)
* ``libxslt`` (Debian package ``libxslt1-dev``)
* ``libenchant-2-2`` (Debian package ``libenchant-2-2``)
* ``libenchant1c2a`` (Debian package ``libenchant1c2a``)
* ``msgfmt`` (Debian package ``gettext``)
* ``git``
@@ -51,12 +51,7 @@ the dependencies might fail::
Working with the code
---------------------
If you do not have a recent installation of ``nodejs``, install it now::
curl -sL https://deb.nodesource.com/setup_17.x | sudo -E bash -
sudo apt install nodejs
To make sure it is on your path variable, close and reopen your terminal. Now, install the Python-level dependencies of pretix::
The first thing you need are all the main application's dependencies::
cd src/
pip3 install -e ".[dev]"

View File

@@ -34,7 +34,5 @@ git push
# Unlock Weblate
for c in $COMPONENTS; do
wlc unlock $c;
done
for c in $COMPONENTS; do
wlc pull $c;
done

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__ = "4.5.0.dev0"
__version__ = "4.4.0.dev0"

View File

@@ -734,7 +734,6 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_numbers_prefix_cancellations',
'invoice_numbers_counter_length',
'invoice_attendee_name',
'invoice_event_location',
'invoice_include_expire_date',
'invoice_address_explanation_text',
'invoice_email_attachment',
@@ -764,7 +763,6 @@ class EventSettingsSerializer(SettingsSerializer):
'cancel_allow_user_paid_refund_as_giftcard',
'cancel_allow_user_paid_require_approval',
'change_allow_user_variation',
'change_allow_user_addons',
'change_allow_user_until',
'change_allow_user_price',
'primary_color',

View File

@@ -1428,7 +1428,7 @@ class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
model = InvoiceLine
fields = ('position', 'description', 'item', 'variation', 'attendee_name', 'event_date_from',
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_name', 'fee_type',
'fee_internal_type', 'event_location')
'fee_internal_type')
class InvoiceSerializer(I18nAwareModelSerializer):

View File

@@ -296,14 +296,7 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'theme_round_borders',
'primary_font',
'organizer_logo_image_inherit',
'organizer_logo_image',
'privacy_url',
'cookie_consent',
'cookie_consent_dialog_title',
'cookie_consent_dialog_text',
'cookie_consent_dialog_text_secondary',
'cookie_consent_dialog_button_yes',
'cookie_consent_dialog_button_no',
'organizer_logo_image'
]
def __init__(self, *args, **kwargs):

View File

@@ -69,7 +69,7 @@ class ExportersMixin:
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
if cf.file:
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
return resp
elif not settings.HAS_CELERY:
return Response(

View File

@@ -46,7 +46,7 @@ class PretixBaseConfig(AppConfig):
from . import invoice # NOQA
from . import notifications # NOQA
from . import email # NOQA
from .services import auth, checkin, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
from .services import auth, checkin, checks, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
from .models import _transactions # NOQA
from django.conf import settings

View File

@@ -324,6 +324,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Tax rate'),
_('Tax name'),
_('Event start date'),
_('Date'),
_('Order code'),
_('E-mail address'),
@@ -347,8 +348,6 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Invoice recipient:') + ' ' + _('Beneficiary'),
_('Invoice recipient:') + ' ' + _('Internal reference'),
_('Payment providers'),
_('Event end date'),
_('Location'),
]
p_providers = OrderPayment.objects.filter(
@@ -407,9 +406,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
', '.join([
str(self.providers.get(p, p)) for p in sorted(set((l.payment_providers or '').split(',')))
if p and p != 'free'
]),
date_format(l.event_date_to, "SHORT_DATE_FORMAT") if l.event_date_to else "",
l.event_location or "",
])
]
@cached_property

View File

@@ -55,7 +55,6 @@ class UserSettingsForm(forms.ModelForm):
'pw_current_wrong': _("The current password you entered was not correct."),
'pw_mismatch': _("Please enter the same password twice"),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
'pw_equal': _("Please choose a password different to your current one.")
}
old_pw = forms.CharField(max_length=255,
@@ -159,12 +158,6 @@ class UserSettingsForm(forms.ModelForm):
code='pw_current'
)
if password1 and password1 == old_pw:
raise forms.ValidationError(
self.error_messages['pw_equal'],
code='pw_equal'
)
if password1:
self.instance.set_password(password1)

View File

@@ -395,13 +395,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
return txt
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
tz = self.invoice.event.timezone
show_end_date = (
self.invoice.event.settings.show_date_to and
self.invoice.event.date_to and
self.invoice.event.date_to.astimezone(tz).date() != self.invoice.event.date_from.astimezone(tz).date()
)
if show_end_date:
if self.invoice.event.settings.show_date_to and self.invoice.event.date_to:
p_str = (
shorten(self.invoice.event.name) + '\n' +
pgettext('invoice', '{from_date}\nuntil {to_date}').format(
@@ -556,10 +550,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
for line in self.invoice.lines.all():
if has_taxes:
tdata.append((
Paragraph(
bleach.clean(line.description, tags=['br']).strip().replace('<br>', '<br/>').replace('\n', '<br />\n'),
self.stylesheet['Normal']
),
Paragraph(line.description, self.stylesheet['Normal']),
"1",
localize(line.tax_rate) + " %",
money_filter(line.net_value, self.invoice.event.currency),
@@ -567,10 +558,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
))
else:
tdata.append((
Paragraph(
bleach.clean(line.description, tags=['br']).strip().replace('<br>', '<br/>').replace('\n', '<br />\n'),
self.stylesheet['Normal']
),
Paragraph(line.description, self.stylesheet['Normal']),
"1",
money_filter(line.gross_value, self.invoice.event.currency),
))

View File

@@ -43,7 +43,7 @@ class Command(BaseCommand):
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum('price')).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(Decimal(0)), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
fee_total=Coalesce(
Subquery(
@@ -51,7 +51,7 @@ class Command(BaseCommand):
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum('value')).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(Decimal(0)), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
tx_total=Coalesce(
Subquery(
@@ -59,12 +59,12 @@ class Command(BaseCommand):
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum(F('price') * F('count'))).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(Decimal(0)), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
).annotate(
correct_total=Case(
When(Q(status=Order.STATUS_CANCELED) | Q(status=Order.STATUS_EXPIRED) | Q(require_approval=True),
then=Value(0)),
then=Value(Decimal(0))),
default=F('position_total') + F('fee_total'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
@@ -73,9 +73,6 @@ class Command(BaseCommand):
tx_total=F('correct_total')
).select_related('event')
for o in qs:
if abs(o.tx_total - o.correct_total) < Decimal('0.00001') and abs(o.position_total + o.fee_total - o.total) < Decimal('0.00001'):
# Ignore SQLite which treats Decimals like floats…
continue
print(f"Error in order {o.full_code}: status={o.status}, sum(positions)+sum(fees)={o.position_total + o.fee_total}, "
f"order.total={o.total}, sum(transactions)={o.tx_total}, expected={o.correct_total}")

View File

@@ -208,7 +208,7 @@ def _parse_csp(header):
def _render_csp(h):
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v)
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items())
def _merge_csp(a, b):

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.2.4 on 2021-10-29 09:58
from django.db import migrations, models
import pretix.base.models.base
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0200_transaction'),
]
operations = [
migrations.CreateModel(
name='Check',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('result', models.CharField(max_length=190)),
('check_type', models.CharField(max_length=190)),
('log', models.TextField()),
],
options={
'abstract': False,
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.4 on 2021-11-03 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0200_transaction'),
]
operations = [
migrations.AddField(
model_name='invoiceline',
name='event_location',
field=models.TextField(null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.9 on 2021-11-04 13:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0201_invoiceline_event_location'),
]
operations = [
migrations.AddField(
model_name='user',
name='needs_password_change',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 3.2.2 on 2021-11-08 07:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0202_user_needs_password_change'),
]
operations = [
migrations.AddField(
model_name='orderposition',
name='is_bundled',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,46 +0,0 @@
# Generated by Django 3.2.2 on 2021-11-08 07:51
from django.db import migrations, models
from django.db.models import Count, OuterRef, Subquery
from django.db.models.functions import Coalesce
def fill_is_bundled(apps, schema_editor):
# We cannot really know if a position was bundled or an add-on, but we can at least guess
ItemBundle = apps.get_model("pretixbase", "ItemBundle")
OrderPosition = apps.get_model("pretixbase", "OrderPosition")
for ib in ItemBundle.objects.iterator():
OrderPosition.all.alias(
pos_earlier=Coalesce(Subquery(
OrderPosition.all.filter(
canceled=False,
addon_to=OuterRef('addon_to'),
item=ib.bundled_item,
variation=ib.bundled_variation,
positionid__lt=OuterRef('positionid'),
).values('addon_to').order_by().annotate(c=Count('*')).values('c'),
output_field=models.IntegerField()
), 0)
).filter(
canceled=False,
addon_to__item=ib.base_item,
item=ib.bundled_item,
variation=ib.bundled_variation,
pos_earlier__lt=ib.count,
).update(
is_bundled=True
)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0203_orderposition_is_bundled'),
]
operations = [
migrations.RunPython(
fill_is_bundled,
migrations.RunPython.noop,
),
]

View File

@@ -23,6 +23,7 @@ from ..settings import GlobalSettingsObject_SettingsStore
from .auth import U2FDevice, User, WebAuthnDevice
from .base import CachedFile, LoggedModel, cachedfile_name
from .checkin import Checkin, CheckinList
from .checks import Check
from .customers import Customer
from .devices import Device, Gate
from .event import (

View File

@@ -36,7 +36,7 @@ from django.db import transaction
dirty_transactions = threading.local()
logger = logging.getLogger(__name__)
fail_loudly = os.getenv('PRETIX_DIRTY_TRANSACTIONS_QUIET', 'false' if settings.DEBUG else 'true') not in ('true', 'True', 'on', '1')
fail_loudly = os.getenv('PRETIX_DIRTY_TRANSACTIONS_QUIET', 'false') not in ('true', 'True', 'on', '1')
class DirtyTransactionsForOrderException(Exception):

View File

@@ -113,8 +113,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
:type date_joined: datetime
:param locale: The user's preferred locale code.
:type locale: str
:param needs_password_change: Whether this user's password needs to be changed.
:type needs_password_change: bool
:param timezone: The user's preferred timezone.
:type timezone: str
"""
@@ -132,8 +130,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
verbose_name=_('Is site admin'))
date_joined = models.DateTimeField(auto_now_add=True,
verbose_name=_('Date joined'))
needs_password_change = models.BooleanField(default=False,
verbose_name=_('Force user to select a new password'))
locale = models.CharField(max_length=50,
choices=settings.LANGUAGES,
default=settings.LANGUAGE_CODE,

View File

@@ -26,39 +26,29 @@
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
# This file contains Apache-licensed contributions copyrighted by: Jakob Schnell
#
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# 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.
from enum import Enum
from typing import List
from django.db import models
from django.utils.translation import gettext_lazy as _
from pretix.presale.signals import register_cookie_providers
from pretix.base.models import LoggedModel
class UsageClass(Enum):
FUNCTIONAL = 1
ANALYTICS = 2
MARKETING = 3
SOCIAL = 4
class Check(LoggedModel):
RESULT_OK = 'ok'
RESULT_WARNING = 'warning'
RESULT_ERROR = 'error'
RESULTS = (
(RESULT_OK, _('OK')),
(RESULT_WARNING, _('Warning')),
(RESULT_ERROR, _('Error')),
)
class CookieProvider:
def __init__(self, identifier: str, usage_classes: List[UsageClass], provider_name: str, privacy_url: str = None, **kwargs):
self.identifier = identifier
self.usage_classes = usage_classes
self.provider_name = provider_name
self.privacy_url = privacy_url
def get_cookie_providers(event, request):
c = [
]
for receiver, response in register_cookie_providers.send(event, request=request):
if isinstance(response, list):
c += response
else:
c.append(response)
c.sort(key=lambda k: str(k.provider_name))
return c
created = models.DateTimeField(auto_now_add=True)
result = models.CharField(max_length=190, choices=RESULTS)
check_type = models.CharField(max_length=190)
log = models.TextField()

View File

@@ -565,8 +565,6 @@ class Event(EventMixin, LoggedModel):
self.settings.ticketoutput_pdf__enabled = True
self.settings.ticketoutput_passbook__enabled = True
self.settings.event_list_type = 'calendar'
self.settings.invoice_email_attachment = True
self.settings.name_scheme = 'given_family'
@property
def social_image(self):

View File

@@ -237,7 +237,7 @@ class Invoice(models.Model):
def _get_invoice_number_from_order(self):
return '{order}-{count}'.format(
order=self.order.code,
count=Invoice.objects.filter(event=self.event, prefix=self.prefix, invoice_no__startswith=f"{self.order.code}-", order=self.order).count() + 1,
count=Invoice.objects.filter(event=self.event, order=self.order).count() + 1,
)
def save(self, *args, **kwargs):
@@ -264,7 +264,6 @@ class Invoice(models.Model):
self.invoice_no = self._get_invoice_number_from_order()
try:
with transaction.atomic():
self.full_invoice_no = self.prefix + self.invoice_no
return super().save(*args, **kwargs)
except DatabaseError:
# Suppress duplicate key errors and try again
@@ -329,8 +328,6 @@ class InvoiceLine(models.Model):
:type event_date_from: datetime
:param event_date_to: Event end date of the (sub)event at the time the invoice was created
:type event_date_to: datetime
:param event_location: Event location of the (sub)event at the time the invoice was created
:type event_location: str
:param item: The item this line refers to
:type item: Item
:param variation: The variation this line refers to
@@ -348,7 +345,6 @@ class InvoiceLine(models.Model):
subevent = models.ForeignKey('SubEvent', null=True, blank=True, on_delete=models.PROTECT)
event_date_from = models.DateTimeField(null=True)
event_date_to = models.DateTimeField(null=True)
event_location = models.TextField(null=True, blank=True)
item = models.ForeignKey('Item', null=True, blank=True, on_delete=models.PROTECT)
variation = models.ForeignKey('ItemVariation', null=True, blank=True, on_delete=models.PROTECT)
attendee_name = models.TextField(null=True, blank=True)

View File

@@ -75,7 +75,7 @@ from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import Customer, User
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.locking import LOCK_TIMEOUT, NoLockManager
from pretix.base.services.locking import NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
@@ -581,7 +581,6 @@ class Order(LockModel, LoggedModel):
Returns whether or not this order can be canceled by the user.
"""
from .checkin import Checkin
from .items import ItemAddOn
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID) or not self.count_positions:
return False
@@ -607,10 +606,7 @@ class Order(LockModel, LoggedModel):
if self.user_change_deadline and now() > self.user_change_deadline:
return False
return (
(self.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])) or
(self.event.settings.change_allow_user_addons and ItemAddOn.objects.filter(base_item_id__in=[op.item_id for op in positions]).exists())
)
return self.event.settings.change_allow_user_variation and any([op.has_variations for op in positions])
@property
@scopes_disabled()
@@ -1310,7 +1306,6 @@ class AbstractPosition(models.Model):
seat = models.ForeignKey(
'Seat', null=True, blank=True, on_delete=models.PROTECT
)
is_bundled = models.BooleanField(default=False)
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True)
street = models.TextField(verbose_name=_('Address'), blank=True, null=True)
@@ -1547,7 +1542,7 @@ class OrderPayment(models.Model):
return self.order.event.get_payment_providers(cached=True).get(self.provider)
@transaction.atomic()
def _mark_paid_inner(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
def _mark_paid(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
from pretix.base.signals import order_paid
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date, force=force)
if can_be_paid is not True:
@@ -1623,6 +1618,10 @@ class OrderPayment(models.Model):
:type mail_text: str
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
"""
from pretix.base.services.invoices import (
generate_invoice, invoice_qualified,
)
with transaction.atomic():
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
if locked_instance.state == self.PAYMENT_STATE_CONFIRMED:
@@ -1666,15 +1665,7 @@ class OrderPayment(models.Model):
))
return
self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum)
def _mark_order_paid(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
ignore_date=False, lock=True, payment_refund_sum=0):
from pretix.base.services.invoices import (
generate_invoice, invoice_qualified,
)
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(seconds=LOCK_TIMEOUT * 2)) or not lock:
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock:
# Performance optimization. In this case, there's really no reason to lock everything and an atomic
# database transaction is more than enough.
lockfn = NoLockManager
@@ -1682,8 +1673,8 @@ class OrderPayment(models.Model):
lockfn = self.order.event.lock
with lockfn():
self._mark_paid_inner(force, count_waitinglist, user, auth, overpaid=payment_refund_sum > self.order.total,
ignore_date=ignore_date)
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total,
ignore_date=ignore_date)
invoice = None
if invoice_qualified(self.order):
@@ -2571,6 +2562,7 @@ class CartPosition(AbstractPosition):
max_digits=10, decimal_places=2,
null=True, blank=True
)
is_bundled = models.BooleanField(default=False)
objects = ScopedManager(organizer='event__organizer')

View File

@@ -97,21 +97,10 @@ class Organizer(LoggedModel):
return self.name
def save(self, *args, **kwargs):
is_new = not self.pk
obj = super().save(*args, **kwargs)
if is_new:
self.set_defaults()
else:
self.get_cache().clear()
self.get_cache().clear()
return obj
def set_defaults(self):
"""
This will be called after organizer creation.
This way, we can use this to introduce new default settings to pretix that do not affect existing organizers.
"""
self.settings.cookie_consent = True
def get_cache(self):
"""
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to

View File

@@ -736,11 +736,7 @@ def process_exit_all(sender, **kwargs):
exit_all_at__isnull=False
).select_related('event', 'event__organizer')
for cl in qs:
positions = cl.positions_inside.filter(
Q(last_exit__isnull=True) | Q(last_exit__lte=cl.exit_all_at),
last_entry__lte=cl.exit_all_at,
)
for p in positions:
for p in cl.positions_inside:
with scope(organizer=cl.event.organizer):
ci = Checkin.objects.create(
position=p, list=cl, auto_checked_in=True, type=Checkin.TYPE_EXIT, datetime=cl.exit_all_at
@@ -752,9 +748,6 @@ def process_exit_all(sender, **kwargs):
cl.event.settings.delete(f'autocheckin_dst_hack_{cl.pk}')
try:
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time()), cl.event.timezone)
except pytz.exceptions.AmbiguousTimeError:
cl.exit_all_at = make_aware(datetime.combine(d.date() + timedelta(days=1), d.time()), cl.event.timezone,
is_dst=False)
except pytz.exceptions.NonExistentTimeError:
cl.event.settings.set(f'autocheckin_dst_hack_{cl.pk}', True)
d += timedelta(hours=1)

View File

@@ -0,0 +1,191 @@
#
# 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/>.
#
import logging
from datetime import timedelta
from django.db import models
from django.db.models import Case, F, OuterRef, Q, Subquery, Sum, Value, When
from django.db.models.functions import Cast, Coalesce, StrIndex, Substr
from django.dispatch import receiver
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import Check, Invoice, Order, OrderFee, OrderPosition
from pretix.base.models.orders import Transaction
from pretix.base.signals import periodic_task
from pretix.celery_app import app
from pretix.helpers.periodic import minimum_interval
logger = logging.getLogger(__name__)
def check_order_transactions():
qs = Order.objects.annotate(
position_total=Coalesce(
Subquery(
OrderPosition.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum('price')).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
fee_total=Coalesce(
Subquery(
OrderFee.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum('value')).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
tx_total=Coalesce(
Subquery(
Transaction.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum(F('price') * F('count'))).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
).annotate(
correct_total=Case(
When(Q(status=Order.STATUS_CANCELED) | Q(status=Order.STATUS_EXPIRED) | Q(require_approval=True),
then=0),
default=F('position_total') + F('fee_total'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
).exclude(
tx_total=F('correct_total')
).select_related('event')
for o in qs:
yield [
Check.RESULT_ERROR,
f'Order {o.full_code} has a wrong total: Status is {o.status} and sum of positions and fees is '
f'{o.position_total + o.fee_total}, so sum of transactions should be {o.correct_total} but is {o.tx_total}'
]
yield [
Check.RESULT_OK,
'Check completed.'
]
def check_order_total():
qs = Order.objects.annotate(
position_total=Coalesce(
Subquery(
OrderPosition.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum('price')).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
fee_total=Coalesce(
Subquery(
OrderFee.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum('value')).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
).exclude(
total=F('position_total') + F('fee_total'),
).select_related('event')
for o in qs:
if o.total != o.position_total + o.fee_total:
yield [
Check.RESULT_ERROR,
f'Order {o.full_code} has a wrong total: Sum of positions and fees is '
f'{o.position_total + o.fee_total}, but total is {o.total}'
]
yield [
Check.RESULT_OK,
'Check completed.'
]
def check_invoice_gaps():
group_qs = Invoice.objects.annotate(
sub_prefix=Substr('invoice_no', 1, StrIndex('invoice_no', Value('-'))),
).order_by().values(
'organizer', 'prefix', 'sub_prefix', 'organizer__slug'
)
for g in group_qs:
numbers = Invoice.objects.filter(
prefix=g['prefix'], organizer=g['organizer']
)
if g['sub_prefix']:
numbers = numbers.filter(invoice_no__startswith=g['sub_prefix']).alias(
real_number=Cast(Substr('invoice_no', StrIndex('invoice_no', Value('-')) + 1), models.IntegerField())
).order_by('real_number')
else:
numbers = numbers.exclude(invoice_no__contains='-').order_by('invoice_no')
numbers = list(numbers.values_list('invoice_no', flat=True))
previous_n = "(initial state)"
previous_numeric_part = 0
for n in numbers:
numeric_part = int(n.split("-")[-1])
if numeric_part != previous_numeric_part + 1:
print(g)
yield [
Check.RESULT_WARNING,
f'Organizer {g["organizer__slug"]}, prefix {g["prefix"]}, invoice {n} follows on {previous_n} with gap'
]
previous_n = n
previous_numeric_part = numeric_part
yield [
Check.RESULT_OK,
'Check completed.'
]
@app.task()
@scopes_disabled()
def run_default_consistency_checks():
check_functions = [
('pretix.orders.transactions', check_order_transactions),
('pretix.orders.total', check_order_total),
('pretix.invoices.gaps', check_invoice_gaps),
]
for check_type, check_function in check_functions:
r = Check.RESULT_OK
log = []
try:
for result, logline in check_function():
if result == Check.RESULT_WARNING and r == Check.RESULT_OK:
r = Check.RESULT_WARNING
elif result == Check.RESULT_ERROR:
r = Check.RESULT_ERROR
log.append(f'[{result}] {logline}')
except Exception as e:
logger.exception('Could not run consistency check')
r = Check.RESULT_ERROR
log.append(f'[error] Check aborted: {e}')
Check.objects.create(result=r, check_type=check_type, log='\n'.join(log))
Check.objects.filter(created__lt=now() - timedelta(days=90)).delete()
@receiver(signal=periodic_task)
@minimum_interval(minutes_after_success=24 * 60)
def periodic_consistency_checks(sender, **kwargs):
run_default_consistency_checks.apply_async()

View File

@@ -69,10 +69,6 @@ from pretix.helpers.models import modelcopy
logger = logging.getLogger(__name__)
def _location_oneliner(loc):
return ', '.join([l.strip() for l in loc.splitlines() if l and l.strip()])
@transaction.atomic
def build_invoice(invoice: Invoice) -> Invoice:
invoice.locale = invoice.event.settings.get('invoice_language', invoice.event.settings.locale)
@@ -180,38 +176,19 @@ def build_invoice(invoice: Invoice) -> Invoice:
reverse_charge = False
positions.sort(key=lambda p: p.sort_key)
fees = list(invoice.order.fees.all())
locations = {
str((p.subevent or invoice.event).location) if (p.subevent or invoice.event).location else None
for p in positions
}
if fees and invoice.event.has_subevents:
locations.add(None)
tax_texts = []
if invoice.event.settings.invoice_event_location and len(locations) == 1 and list(locations)[0] is not None:
tax_texts.append(pgettext("invoice", "Event location: {location}").format(
location=_location_oneliner(str(list(locations)[0]))
))
for i, p in enumerate(positions):
if not invoice.event.settings.invoice_include_free and p.price == Decimal('0.00') and not p.addon_c:
continue
location = str((p.subevent or invoice.event).location) if (p.subevent or invoice.event).location else None
desc = str(p.item.name)
if p.variation:
desc += " - " + str(p.variation.value)
if p.addon_to_id:
desc = " + " + desc
if invoice.event.settings.invoice_attendee_name and p.attendee_name:
desc += "<br />" + pgettext("invoice", "Attendee: {name}").format(
name=p.attendee_name
)
desc += "<br />" + pgettext("invoice", "Attendee: {name}").format(name=p.attendee_name)
for recv, resp in invoice_line_text.send(sender=invoice.event, position=p):
if resp:
desc += "<br/>" + resp
@@ -227,12 +204,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
if invoice.event.has_subevents:
desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent)
if invoice.event.settings.invoice_event_location and location and len(locations) > 1:
desc += "<br />" + pgettext("invoice", "Event location: {location}").format(
location=_location_oneliner(location)
)
InvoiceLine.objects.create(
position=i,
invoice=invoice,
@@ -245,7 +216,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
attendee_name=p.attendee_name if invoice.event.settings.invoice_attendee_name else None,
event_date_from=p.subevent.date_from if invoice.event.has_subevents else invoice.event.date_from,
event_date_to=p.subevent.date_to if invoice.event.has_subevents else invoice.event.date_to,
event_location=location if invoice.event.settings.invoice_event_location else None,
tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
)
@@ -258,7 +228,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
tax_texts.append(tax_text)
offset = len(positions)
for i, fee in enumerate(fees):
for i, fee in enumerate(invoice.order.fees.all()):
if fee.fee_type == OrderFee.FEE_TYPE_OTHER and fee.description:
fee_title = fee.description
else:
@@ -272,12 +242,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
gross_value=fee.value,
event_date_from=None if invoice.event.has_subevents else invoice.event.date_from,
event_date_to=None if invoice.event.has_subevents else invoice.event.date_to,
event_location=(
None if invoice.event.has_subevents
else (str(invoice.event.location)
if invoice.event.settings.invoice_event_location and invoice.event.location
else None)
),
tax_value=fee.tax_value,
tax_rate=fee.tax_rate,
tax_name=fee.tax_rule.name if fee.tax_rule else '',

View File

@@ -35,7 +35,7 @@
import json
import logging
from collections import Counter, defaultdict, namedtuple
from collections import Counter, namedtuple
from datetime import datetime, time, timedelta
from decimal import Decimal
from typing import List, Optional
@@ -46,7 +46,7 @@ from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import (
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, Sum, Value,
Exists, F, IntegerField, Max, Min, OuterRef, Q, Sum, Value,
)
from django.db.models.functions import Coalesce, Greatest
from django.db.transaction import get_connection
@@ -73,7 +73,7 @@ from pretix.base.models.orders import (
InvoiceAddress, OrderFee, OrderRefund, generate_secret,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.models.tax import TaxRule
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.secrets import assign_ticket_secret
@@ -122,7 +122,8 @@ error_messages = {
'from your cart.'),
'voucher_invalid_item': _('The voucher code used for one of the items in your cart is not valid for this item. We '
'removed this item from your cart.'),
'voucher_required': _('You need a valid voucher code to order one of the products.'),
'voucher_required': _('You need a valid voucher code to order one of the products in your cart. We removed this '
'item from your cart.'),
'some_subevent_not_started': _('The presale period for one of the events in your cart has not yet started. The '
'affected positions have been removed from your cart.'),
'some_subevent_ended': _('The presale period for one of the events in your cart has ended. The affected '
@@ -130,13 +131,6 @@ error_messages = {
'seat_invalid': _('One of the seats in your order was invalid, we removed the position from your cart.'),
'seat_unavailable': _('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
'country_blocked': _('One of the selected products is not available in the selected country.'),
'not_for_sale': _('You selected a product which is not available for sale.'),
'addon_invalid_base': _('You can not select an add-on for the selected product.'),
'addon_duplicate_item': _('You can not select two variations of the same add-on product.'),
'addon_max_count': _('You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.'),
'addon_min_count': _('You need to select at least %(min)s add-ons from the category %(cat)s for the '
'product %(base)s.'),
'addon_no_multi': _('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
}
logger = logging.getLogger(__name__)
@@ -1267,15 +1261,15 @@ class OrderChangeManager:
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
SeatOperation = namedtuple('SubeventOperation', ('position', 'seat'))
PriceOperation = namedtuple('PriceOperation', ('position', 'price', 'price_diff'))
PriceOperation = namedtuple('PriceOperation', ('position', 'price'))
TaxRuleOperation = namedtuple('TaxRuleOperation', ('position', 'tax_rule'))
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
CancelOperation = namedtuple('CancelOperation', ('position',))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership'))
SplitOperation = namedtuple('SplitOperation', ('position',))
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee', 'price_diff'))
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value'))
AddFeeOperation = namedtuple('AddFeeOperation', ('fee',))
CancelFeeOperation = namedtuple('CancelFeeOperation', ('fee',))
RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',))
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True):
@@ -1392,7 +1386,7 @@ class OrderChangeManager:
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'):
self._invoice_dirty = True
self._operations.append(self.PriceOperation(position, price, price.gross - position.price))
self._operations.append(self.PriceOperation(position, price))
def change_tax_rule(self, position_or_fee, tax_rule: TaxRule):
self._operations.append(self.TaxRuleOperation(position_or_fee, tax_rule))
@@ -1432,28 +1426,28 @@ class OrderChangeManager:
new_tax = tax_rule.tax(pos.price, base_price_is='gross', currency=self.event.currency,
override_tax_rate=new_rate)
self._totaldiff += new_tax.gross - pos.price
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
self._operations.append(self.PriceOperation(pos, new_tax))
def cancel_fee(self, fee: OrderFee):
self._totaldiff -= fee.value
self._operations.append(self.CancelFeeOperation(fee, -fee.value))
self._operations.append(self.CancelFeeOperation(fee))
self._invoice_dirty = True
def add_fee(self, fee: OrderFee):
self._totaldiff += fee.value
self._invoice_dirty = True
self._operations.append(self.AddFeeOperation(fee, fee.value))
self._operations.append(self.AddFeeOperation(fee))
def change_fee(self, fee: OrderFee, value: Decimal):
value = (fee.tax_rule or TaxRule.zero()).tax(value, base_price_is='gross')
self._totaldiff += value.gross - fee.value
self._invoice_dirty = True
self._operations.append(self.FeeValueOperation(fee, value, value.gross - fee.value))
self._operations.append(self.FeeValueOperation(fee, value))
def cancel(self, position: OrderPosition):
self._totaldiff -= position.price
self._quotadiff.subtract(position.quotas)
self._operations.append(self.CancelOperation(position, -position.price))
self._operations.append(self.CancelOperation(position))
if position.seat:
self._seatdiff.subtract([position.seat])
@@ -1478,7 +1472,7 @@ class OrderChangeManager:
try:
if price is None:
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
elif not isinstance(price, TaxedPrice):
else:
price = item.tax(price, base_price_is='gross', invoice_address=self._invoice_address)
except TaxRule.SaleNotAllowed:
raise OrderError(self.error_messages['tax_rule_country_blocked'])
@@ -1521,190 +1515,6 @@ class OrderChangeManager:
self._operations.append(self.SplitOperation(position))
def set_addons(self, addons):
if self._operations:
raise ValueError("Setting addons should be the first/only operation")
# Prepare various containers to hold data later
current_addons = defaultdict(lambda: defaultdict(list)) # OrderPos -> currently attached add-ons
input_addons = defaultdict(Counter) # OrderPos -> final desired set of add-ons
selected_addons = defaultdict(Counter) # OrderPos, ItemAddOn -> final desired set of add-ons
opcache = {} # OrderPos.pk -> OrderPos
quota_diff = Counter() # Quota -> Number of usages
available_categories = defaultdict(set) # OrderPos -> Category IDs to choose from
price_included = defaultdict(dict) # OrderPos -> CategoryID -> bool(price is included)
toplevel_op = self.order.positions.filter(
addon_to__isnull=True
).prefetch_related(
'addons', 'item__addons', 'item__addons__addon_category'
).select_related('item', 'variation')
_items_cache = {
i.pk: i
for i in self.event.items.select_related('category').prefetch_related(
'addons', 'bundles', 'addons__addon_category', 'quotas'
).annotate(
has_variations=Count('variations'),
).filter(
id__in=[a['item'] for a in addons]
).order_by()
}
_variations_cache = {
v.pk: v
for v in ItemVariation.objects.filter(item__event=self.event).prefetch_related(
'quotas'
).select_related('item', 'item__event').filter(
id__in=[a['variation'] for a in addons if a.get('variation')]
).order_by()
}
# Prefill some of the cache containers
for op in toplevel_op:
if op.canceled:
continue
available_categories[op.pk] = {iao.addon_category_id for iao in op.item.addons.all()}
price_included[op.pk] = {iao.addon_category_id: iao.price_included for iao in op.item.addons.all()}
opcache[op.pk] = op
for a in op.addons.all():
if a.canceled:
continue
if not a.is_bundled:
current_addons[op][a.item_id, a.variation_id].append(a)
# Create operations, perform various checks
for a in addons:
# Check whether the specified items are part of what we just fetched from the database
# If they are not, the user supplied item IDs which either do not exist or belong to
# a different event
if a['item'] not in _items_cache or (a['variation'] and a['variation'] not in _variations_cache):
raise OrderError(error_messages['not_for_sale'])
# Only attach addons to things that are actually in this user's cart
if a['addon_to'] not in opcache:
raise OrderError(error_messages['addon_invalid_base'])
op = opcache[a['addon_to']]
item = _items_cache[a['item']]
variation = _variations_cache[a['variation']] if a['variation'] is not None else None
if item.category_id not in available_categories[op.pk]:
raise OrderError(error_messages['addon_invalid_base'])
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
quotas = list(item.quotas.filter(subevent=op.subevent)
if variation is None else variation.quotas.filter(subevent=op.subevent))
if not quotas:
raise OrderError(error_messages['unavailable'])
if (a['item'], a['variation']) in input_addons[op.id]:
raise OrderError(error_messages['addon_duplicate_item'])
if item.require_voucher or op.item.hide_without_voucher or (op.variation and op.variation.hide_without_voucher):
raise OrderError(error_messages['voucher_required'])
if not item.is_available() or (variation and not variation.is_available()):
raise OrderError(error_messages['unavailable'])
if self.order.sales_channel not in item.sales_channels or (
variation and self.order.sales_channel not in variation.sales_channels):
raise OrderError(error_messages['unavailable'])
if op.subevent and item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
raise OrderError(error_messages['not_for_sale'])
if op.subevent and variation and variation.pk in op.subevent.var_overrides and \
not op.subevent.var_overrides[variation.pk].is_available():
raise OrderError(error_messages['not_for_sale'])
if item.has_variations and not variation:
raise OrderError(error_messages['not_for_sale'])
if variation and variation.item_id != item.pk:
raise OrderError(error_messages['not_for_sale'])
if op.subevent and op.subevent.presale_start and now() < op.subevent.presale_start:
raise OrderError(error_messages['not_started'])
if (op.subevent and op.subevent.presale_has_ended) or self.event.presale_has_ended:
raise OrderError(error_messages['ended'])
if item.require_bundling:
raise OrderError(error_messages['unavailable'])
input_addons[op.id][a['item'], a['variation']] = a.get('count', 1)
selected_addons[op.id, item.category_id][a['item'], a['variation']] = a.get('count', 1)
if price_included[op.pk].get(item.category_id):
price = TAXED_ZERO
else:
price = get_price(
item, variation, voucher=None, custom_price=a.get('price'), subevent=op.subevent,
custom_price_is_net=self.event.settings.display_net_prices,
invoice_address=self._invoice_address,
)
if a.get('count', 1) > len(current_addons[op][a['item'], a['variation']]):
# This add-on is new, add it to the cart
for quota in quotas:
quota_diff[quota] += a.get('count', 1) - len(current_addons[op][a['item'], a['variation']])
for i in range(a.get('count', 1) - len(current_addons[op][a['item'], a['variation']])):
self.add_position(
item=item, variation=variation, price=price,
addon_to=op, subevent=op.subevent, seat=None,
)
# Check constraints on the add-on combinations
for op in toplevel_op:
item = op.item
for iao in item.addons.all():
selected = selected_addons[op.id, iao.addon_category_id]
n_per_i = Counter()
for (i, v), c in selected.items():
n_per_i[i] += c
if sum(selected.values()) > iao.max_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise OrderError(
error_messages['addon_max_count'],
{
'base': str(item.name),
'max': iao.max_count,
'cat': str(iao.addon_category.name),
}
)
elif sum(selected.values()) < iao.min_count:
# TODO: Proper i18n
# TODO: Proper pluralization
raise OrderError(
error_messages['addon_min_count'],
{
'base': str(item.name),
'min': iao.min_count,
'cat': str(iao.addon_category.name),
}
)
elif any(v > 1 for v in n_per_i.values()) and not iao.multi_allowed:
raise OrderError(
error_messages['addon_no_multi'],
{
'base': str(item.name),
'cat': str(iao.addon_category.name),
}
)
# Detect removed add-ons and create RemoveOperations
for cp, al in list(current_addons.items()):
for k, v in al.items():
input_num = input_addons[cp.id].get(k, 0)
current_num = len(current_addons[cp].get(k, []))
if input_num < current_num:
for a in current_addons[cp][k][:current_num - input_num]:
if a.canceled:
continue
self.cancel(a)
def _check_seats(self):
for seat, diff in self._seatdiff.items():
if diff <= 0:

View File

@@ -20,13 +20,11 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import os
import re
from urllib.error import HTTPError
import vat_moss.errors
import vat_moss.id
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from zeep import Client, Transport
from zeep.cache import SqliteCache
@@ -85,12 +83,14 @@ def _validate_vat_id_CH(vat_id, country_code):
vat_id = re.sub('[^A-Z0-9]', '', vat_id.replace('HR', '').replace('MWST', ''))
try:
transport = Transport(cache=SqliteCache(os.path.join(settings.CACHE_DIR, "validate_vat_id_ch_zeep_cache.db")))
transport = Transport(cache=SqliteCache())
client = Client(
'https://www.uid-wse.admin.ch/V5.0/PublicServices.svc?wsdl',
'https://www.uid-wse-a.admin.ch/V5.0/PublicServices.svc?wsdl',
transport=transport
)
result = client.service.ValidateUID(uid=vat_id)
if not client.service.ValidateUID(uid=vat_id):
raise VATIDFinalError(_('This VAT ID is not valid. Please re-check your input.'))
return vat_id
except Fault as e:
if e.message == 'Data_validation_failed':
raise VATIDFinalError(_('This VAT ID is not valid. Please re-check your input.'))
@@ -118,10 +118,6 @@ def _validate_vat_id_CH(vat_id, country_code):
'need to charge VAT on your invoice. You can get the tax amount '
'back via the VAT reimbursement process.'
))
else:
if not result:
raise VATIDFinalError(_('This VAT ID is not valid. Please re-check your input.'))
return vat_id
def validate_vat_id(vat_id, country_code):

View File

@@ -310,17 +310,6 @@ DEFAULTS = {
label=_("Show attendee names on invoices"),
)
},
'invoice_event_location': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Show event location on invoices"),
help_text=_("The event location will be shown below the list of products if it is the same for all "
"lines. It will be shown on every line if there are different locations.")
)
},
'invoice_eu_currencies': {
'default': 'True',
'type': bool,
@@ -426,7 +415,7 @@ DEFAULTS = {
)
},
'invoice_include_expire_date': {
'default': 'False', # default for new events is True
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
@@ -482,7 +471,7 @@ DEFAULTS = {
)
},
'invoice_renderer': {
'default': 'classic', # default for new events is 'modern1'
'default': 'classic',
'type': str,
},
'ticket_secret_generator': {
@@ -908,7 +897,7 @@ DEFAULTS = {
'type': str
},
'invoice_email_attachment': {
'default': 'False', # default for new events is True
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
@@ -1241,7 +1230,7 @@ DEFAULTS = {
)
},
'event_list_type': {
'default': 'list', # default for new events is 'calendar'
'default': 'list',
'type': str,
'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField,
@@ -1301,15 +1290,6 @@ DEFAULTS = {
label=_("Customers can change the variation of the products they purchased"),
)
},
'change_allow_user_addons': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Customers can change their selected add-on products"),
)
},
'change_allow_user_price': {
'default': 'gte',
'type': str,
@@ -1512,17 +1492,6 @@ DEFAULTS = {
),
'serializer_class': serializers.URLField,
},
'privacy_url': {
'default': None,
'type': str,
'form_class': forms.URLField,
'form_kwargs': dict(
label=_("Privacy Policy URL"),
help_text=_("This should point e.g. to a part of your website that explains how you use data gathered in "
"your ticket shop."),
),
'serializer_class': serializers.URLField,
},
'confirm_texts': {
'default': LazyI18nStringList(),
'type': LazyI18nStringList,
@@ -2005,7 +1974,7 @@ Your {organizer} team"""))
),
},
'theme_color_success': {
'default': '#50a167',
'default': '#50A167',
'type': str,
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
@@ -2027,7 +1996,7 @@ Your {organizer} team"""))
),
},
'theme_color_danger': {
'default': '#c44f4f',
'default': '#C44F4F',
'type': str,
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
@@ -2049,7 +2018,7 @@ Your {organizer} team"""))
),
},
'theme_color_background': {
'default': '#f5f5f5',
'default': '#FFFFFF',
'type': str,
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
@@ -2475,7 +2444,7 @@ Your {organizer} team"""))
)
},
'name_scheme': {
'default': 'full', # default for new events is 'given_family'
'default': 'full',
'type': str
},
'giftcard_length': {
@@ -2500,77 +2469,6 @@ Your {organizer} team"""))
'many years. If you keep it empty, gift cards do not have an explicit expiry date.'),
)
},
'cookie_consent': {
'default': 'False',
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Enable cookie consent management features"),
),
'type': bool,
},
'cookie_consent_dialog_text': {
'default': LazyI18nString.from_gettext(gettext_noop(
'By clicking "Accept all cookies", you agree to the storing of cookies and use of similar technologies on '
'your device.'
)),
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Dialog text"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '3', 'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
'cookie_consent_dialog_text_secondary': {
'default': LazyI18nString.from_gettext(gettext_noop(
'We use cookies and similar technologies to gather data that allows us to improve this website and our '
'offerings. If you do not agree, we will only use cookies if they are essential to providing the services '
'this website offers.'
)),
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Secondary dialog text"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '3', 'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
'cookie_consent_dialog_title': {
'default': LazyI18nString.from_gettext(gettext_noop('Privacy settings')),
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_('Dialog title'),
widget=I18nTextInput,
widget_kwargs={'attrs': {'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
'cookie_consent_dialog_button_yes': {
'default': LazyI18nString.from_gettext(gettext_noop('Accept all cookies')),
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_('"Accept" button description'),
widget=I18nTextInput,
widget_kwargs={'attrs': {'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
'cookie_consent_dialog_button_no': {
'default': LazyI18nString.from_gettext(gettext_noop('Required cookies only')),
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_('"Reject" button description'),
widget=I18nTextInput,
widget_kwargs={'attrs': {'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
'seating_choice': {
'default': 'True',
'form_class': forms.BooleanField,

View File

@@ -14,17 +14,16 @@
</o:OfficeDocumentSettings>
</xml><![endif]-->
<style type="text/css">
body, .container {
body {
background-color: #eee;
background-position: top;
background-repeat: repeat-x;
font-family: "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.4;
color: #333;
margin: 0;
padding: 0;
}
.container {
padding: 20px;
padding-top: 20px;
}
table.layout > tr > td,
@@ -37,8 +36,7 @@
table.layout > tr > td.logo,
table.layout > tbody > tr > td.logo,
table.layout > thead > tr > td.logo {
padding: {% if event.settings.logo_image_large %}0 0 0 0{% else %}20px 0 0 0{% endif %};
mso-line-height-rule: at-least;
padding: 20px 0 0 0;
}
table.layout > tr > td.header,
@@ -114,6 +112,10 @@
font-size: 12px;
}
.content {
padding: 0 18px;
}
::selection {
background: {{ color }};
color: #FFF;
@@ -147,7 +149,7 @@
table.layout > tr > td.containertd,
table.layout > tbody > tr > td.containertd,
table.layout > thead > tr > td.containertd {
padding: 20px;
padding: 15px 0;
}
a.button {
@@ -165,8 +167,7 @@
}
.order-button {
padding-top: 5px;
text-align: center;
padding-top: 5px
}
.order-button a.button {
font-size: 12px;
@@ -213,14 +214,14 @@
<![endif]-->
</head>
<body align="center">
<table width="100%"><tr><td align="center" class="container">
<!--[if gte mso 9]>
<table width="600"><tr><td align="center">
<table width="100%"><tr><td align="center">
<table width="600"><tr><td align="center"
<![endif]-->
<table class="layout" style="max-width:600px" border="0" cellspacing="0">
{% if event.settings.logo_image %}
<tr>
<td align="center" class="logo">
<td style="line-height: 0; {% if event.settings.logo_image_large %}padding: 0;{% endif %}" align="center" class="logo">
{% if event.settings.logo_image_large %}
<img src="{% if event.settings.logo_image|thumb:'600_x5000'|first == '/' %}{{ site_url }}{% endif %}{{ event.settings.logo_image|thumb:'600_x5000' }}" alt="{{ event.name }}" style="width:100%" />
{% else %}
@@ -231,6 +232,9 @@
{% endif %}
<tr>
<td class="header" align="center">
<!--[if gte mso 9]>
<table cellpadding="20"><tr><td align="center">
<![endif]-->
{% if event %}
<h2><a href="{% abseventurl event "presale:event.index" %}" target="_blank">{{ event.name }}</a>
</h2>
@@ -243,30 +247,51 @@
{% block header %}
<h1>{{ subject }}</h1>
{% endblock %}
<!--[if gte mso 9]>
</td></tr></table>
<![endif]-->
</td>
</tr>
<tr>
<td class="containertd">
<!--[if gte mso 9]>
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{{ body|safe }}
</div>
<!--[if gte mso 9]>
</td></tr></table>
<![endif]-->
</td>
</tr>
{% if order %}
<tr>
<td class="order containertd">
<!--[if gte mso 9]>
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{% include "pretixbase/email/order_details.html" %}
</div>
<!--[if gte mso 9]>
</td></tr></table>
<![endif]-->
</td>
</tr>
{% endif %}
{% if signature %}
<tr>
<td class="order containertd">
<!--[if gte mso 9]>
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{{ signature | safe }}
</div>
<!--[if gte mso 9]>
</td></tr></table>
<![endif]-->
</td>
</tr>
{% endif %}
@@ -278,7 +303,7 @@
<br/>
<!--[if gte mso 9]>
</td></tr></table>
<![endif]-->
</td></tr></table>
<![endif]-->
</body>
</html>

View File

@@ -1,29 +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 django import template
register = template.Library()
@register.filter
def classname(obj):
return obj.__class__.__name__

View File

@@ -47,7 +47,7 @@ class DownloadView(TemplateView):
return HttpResponse('1' if self.object.file else '0')
elif self.object.file:
resp = ChunkBasedFileResponse(self.object.file.file, content_type=self.object.type)
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(self.object.filename).encode('ascii', 'ignore')
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(self.object.filename)
return resp
else:
return super().get(request, *args, **kwargs)

View File

@@ -639,7 +639,6 @@ class CancelSettingsForm(SettingsForm):
'change_allow_user_variation',
'change_allow_user_price',
'change_allow_user_until',
'change_allow_user_addons',
]
def __init__(self, *args, **kwargs):
@@ -752,7 +751,6 @@ class InvoiceSettingsForm(SettingsForm):
'invoice_reissue_after_modify',
'invoice_generate',
'invoice_attendee_name',
'invoice_event_location',
'invoice_include_expire_date',
'invoice_numbers_consecutive',
'invoice_numbers_prefix',

View File

@@ -307,14 +307,8 @@ class OrganizerSettingsForm(SettingsForm):
'theme_color_danger',
'theme_color_background',
'theme_round_borders',
'primary_font',
'privacy_url',
'cookie_consent',
'cookie_consent_dialog_title',
'cookie_consent_dialog_text',
'cookie_consent_dialog_text_secondary',
'cookie_consent_dialog_button_yes',
'cookie_consent_dialog_button_no',
'primary_font'
]
organizer_logo_image = ExtFileField(

View File

@@ -70,7 +70,6 @@ class UserEditForm(forms.ModelForm):
'require_2fa',
'is_active',
'is_staff',
'needs_password_change',
'last_login'
]

View File

@@ -69,11 +69,6 @@ class PermissionMiddleware:
"user.settings.notifications.off",
)
EXCEPTIONS_FORCED_PW_CHANGE = (
"user.settings",
"auth.logout"
)
EXCEPTIONS_2FA = (
"user.settings.2fa",
"user.settings.2fa.add",
@@ -135,9 +130,6 @@ class PermissionMiddleware:
if url_name not in ('user.reauth', 'auth.logout'):
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
if request.user.needs_password_change and url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE:
return redirect(reverse('control:user.settings') + '?next=' + quote(request.get_full_path()))
if not request.user.require_2fa and settings.PRETIX_OBLIGATORY_2FA \
and url_name not in self.EXCEPTIONS_2FA:
return redirect(reverse('control:user.settings.2fa'))

View File

@@ -422,6 +422,11 @@ def get_global_navigation(request):
'url': reverse('control:global.license'),
'active': (url.url_name == 'global.license'),
},
{
'label': _('Consistency check'),
'url': reverse('control:global.consistency'),
'active': (url.url_name == 'global.consistency'),
},
]
})

View File

@@ -429,15 +429,6 @@
</div>
{% endif %}
{% if request.user.needs_password_change %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
For security reasons, please change your password before you continue. Afterwards you
will be redirected to your original destination.
{% endblocktrans %}
</div>
{% endif %}
{% block content %}
{% endblock %}
<footer>

View File

@@ -42,37 +42,13 @@
<fieldset>
<legend>{% trans "Order changes" %}</legend>
<div class="alert alert-info">
<p>
{% blocktrans trimmed %}
Allowing customers to change their own orders is a complex process due to the many different options pretix provides. Therefore, this feature currently has the following
limitations:
{% endblocktrans %}
</p>
<ul>
<li>{% trans "It is possible to switch to a different variation of the same product, but not to an entirely different product (except for add-on products)." %}</li>
<li>{% trans "Changing the seat or the event date in an event series will become available in the future, but is not possible now." %}</li>
<li>{% trans "If a change leads to a price change, there will not be a change to fees such as payment, service, or shipping fees, even though an additional payment might be required." %}</li>
<li>{% trans "If an add-on product is newly added, the system currently does not validate if there are required questions or fields that need to be filled out." %}</li>
<li>{% trans "Customers currently cannot switch to a product variation or add an add-on product that requires them to use a voucher or membership." %}</li>
<li>{% trans "Additional constraints and validation steps added by plugins are not enforced." %}</li>
</ul>
{% blocktrans trimmed %}
Allowing users to change their order is a feature under development. Therefore, currently only specific changes (such as changing the variation of a product) are possible. More options might be added later.
{% endblocktrans %}
</div>
{% bootstrap_field form.change_allow_user_variation layout="control" %}
{% bootstrap_field form.change_allow_user_addons layout="control" %}
{% bootstrap_field form.change_allow_user_until layout="control" %}
{% bootstrap_field form.change_allow_user_price layout="control" %}
<div class="alert alert-info">
<p>
{% blocktrans trimmed %}
If the change leads to a price reduction and automatic refunds are enabled for self-service cancellations,
the system will try to refund the money automatically.
{% endblocktrans %}
{% blocktrans trimmed %}
Refunds can be issued as a gift card if the respective option is set, but there is no customer choice between
gift card and direct refund.
{% endblocktrans %}
</p>
</div>
{% bootstrap_field form.change_allow_user_until layout="control" %}
</fieldset>
</div>
<div class="form-group submit-group">

View File

@@ -48,7 +48,6 @@
<legend>{% trans "Invoice customization" %}</legend>
{% bootstrap_field form.invoice_renderer layout="control" %}
{% bootstrap_field form.invoice_attendee_name layout="control" %}
{% bootstrap_field form.invoice_event_location layout="control" %}
{% bootstrap_field form.invoice_include_expire_date layout="control" %}
{% bootstrap_field form.invoice_introductory_text layout="control" %}
{% bootstrap_field form.invoice_additional_text layout="control" %}

View File

@@ -204,7 +204,7 @@
{% bootstrap_field sform.logo_show_title layout="control" %}
{% bootstrap_field sform.og_image layout="control" %}
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
{% propagated request.event org_url "primary_color" "primary_font" "theme_color_success" "theme_color_danger" "theme_color_background" "theme_round_borders" %}
{% propagated request.event org_url "primary_color" "primary_font" "theme_color_success" "theme_color_danger" "theme_round_borders" %}
{% bootstrap_field sform.primary_color layout="control" %}
{% bootstrap_field sform.theme_color_success layout="control" %}
{% bootstrap_field sform.theme_color_danger layout="control" %}

View File

@@ -17,7 +17,7 @@
<div class="help-block">
{% blocktrans trimmed %}
This is the address users can buy your tickets at. Should be short, only contain lowercase
letters, numbers, dots, and dashes, and must be unique among your events. We recommend some kind of
letters and numbers, and must be unique among your events. We recommend some kind of
abbreviation or a date with less than 10 characters that can be easily remembered, but you
can also choose to use a random value.
{% endblocktrans %}

View File

@@ -0,0 +1,33 @@
{% extends "pretixcontrol/global_settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h2>{% trans "Consistency checks" %}</h2>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Check type" %}</th>
<th>{% trans "Result" %}</th>
</tr>
</thead>
<tbody>
{% for r in results %}
<tr class="{% if r.result == "error" %}danger{% elif r.result == "warning" %}warning{% endif %}">
<td>{{ r.created|date:"SHORT_DATETIME_FORMAT" }}</td>
<td>{{ r.check_type }}</td>
<td>{{ r.get_result_display }}</td>
<td>
<a href="{% url "control:global.consistency.detail" pk=r.pk %}" class="btn btn-default">{% trans "Show log" %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends "pretixcontrol/global_settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h2>{% trans "Consistency check" %}</h2>
<p>{{ result.created|date:"SHORT_DATETIME_FORMAT" }}</p>
<p>{{ result.check_type }}</p>
<p>{{ result.get_result_display }}</p>
<pre>{{ result.log }}</pre>
{% endblock %}

View File

@@ -82,49 +82,6 @@
{% bootstrap_field sform.giftcard_expiry_years layout="control" %}
{% bootstrap_field sform.giftcard_length layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Privacy" %}</legend>
{% bootstrap_field sform.privacy_url layout="control" %}
<div class="alert alert-legal">
<p>
{% blocktrans trimmed %}
Some jurisdictions, including the European Union, require user consent before you
are allowed to use cookies or similar technology for analytics, tracking, payment,
or similar purposes.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
pretix itself only ever sets cookies that are required to provide the service
requested by the user or to maintain an appropriate level of security. Therefore,
cookies set by pretix itself do not require consent in all jurisdictions that we
are aware of.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
Therefore, the settings on this page will <strong>only</strong> have an affect
if you use <strong>plugins</strong> that require additional cookies
<strong>and</strong> participate in our cookie consent mechanism.
{% endblocktrans %}
</p>
<p>
<strong>{% blocktrans trimmed %}
Ultimately, it is your responsibility to make sure you comply with all relevant
laws. We try to help by providing these settings, but we cannot assume liability
since we do not know the exact configuration of your pretix usage, the legal details
in your specific jurisdiction, or the agreements you have with third parties such as
payment or tracking providers.
{% endblocktrans %}</strong>
</p>
</div>
{% bootstrap_field sform.cookie_consent layout="control" %}
{% bootstrap_field sform.cookie_consent_dialog_title layout="control" %}
{% bootstrap_field sform.cookie_consent_dialog_text layout="control" %}
{% bootstrap_field sform.cookie_consent_dialog_text_secondary layout="control" %}
{% bootstrap_field sform.cookie_consent_dialog_button_yes layout="control" %}
{% bootstrap_field sform.cookie_consent_dialog_button_no layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Invoices" %}</legend>
{% bootstrap_field sform.invoice_regenerate_allowed layout="control" %}

View File

@@ -19,7 +19,6 @@
{% bootstrap_field form.email layout='control' %}
{% bootstrap_field form.new_pw layout='control' %}
{% bootstrap_field form.new_pw_repeat layout='control' %}
{% bootstrap_field form.needs_password_change layout='control' %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -45,7 +45,6 @@
{% endif %}
{% bootstrap_field form.last_login layout='control' %}
{% bootstrap_field form.require_2fa layout='control' %}
{% bootstrap_field form.needs_password_change layout='control' %}
</fieldset>
<fieldset>
<legend>{% trans "Team memberships" %}</legend>

View File

@@ -55,6 +55,8 @@ urlpatterns = [
re_path(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='global.settings'),
re_path(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
re_path(r'^global/license/$', global_settings.LicenseCheckView.as_view(), name='global.license'),
re_path(r'^global/consistency/$', global_settings.ConsistencyCheckListView.as_view(), name='global.consistency'),
re_path(r'^global/consistency/(?P<pk>\d+)/$', global_settings.ConsistencyCheckDetailView.as_view(), name='global.consistency.detail'),
re_path(r'^global/message/$', global_settings.MessageView.as_view(), name='global.message'),
re_path(r'^logdetail/$', global_settings.LogDetailView.as_view(), name='global.logdetail'),
re_path(r'^logdetail/payment/$', global_settings.PaymentDetailView.as_view(), name='global.paymentdetail'),

View File

@@ -39,9 +39,9 @@ from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic import FormView, TemplateView
from django.views.generic import FormView, ListView, TemplateView, DetailView
from pretix.base.models import LogEntry, OrderPayment, OrderRefund
from pretix.base.models import Check, LogEntry, OrderPayment, OrderRefund
from pretix.base.services.update_check import check_result_table, update_check
from pretix.base.settings import GlobalSettingsObject
from pretix.control.forms.global_settings import (
@@ -265,3 +265,15 @@ class LicenseCheckView(StaffMemberRequiredMixin, FormView):
))
return res
class ConsistencyCheckListView(AdministratorPermissionRequiredMixin, ListView):
queryset = Check.objects.order_by('-created').defer('log')
context_object_name = 'results'
template_name = 'pretixcontrol/global_consistency.html'
class ConsistencyCheckDetailView(AdministratorPermissionRequiredMixin, DetailView):
queryset = Check.objects.all()
context_object_name = 'result'
template_name = 'pretixcontrol/global_consistency_detail.html'

View File

@@ -401,10 +401,10 @@ class QuestionList(ListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
questions = []
ctx['questions'] = list(ctx['questions'])
if self.request.event.settings.attendee_names_asked:
questions.append(
ctx['questions'].append(
FakeQuestion(
id='attendee_name_parts',
question=_('Attendee name'),
@@ -416,7 +416,7 @@ class QuestionList(ListView):
)
if self.request.event.settings.attendee_emails_asked:
questions.append(
ctx['questions'].append(
FakeQuestion(
id='attendee_email',
question=_('Attendee email'),
@@ -427,8 +427,8 @@ class QuestionList(ListView):
)
)
if self.request.event.settings.attendee_company_asked:
questions.append(
if self.request.event.settings.attendee_emails_asked:
ctx['questions'].append(
FakeQuestion(
id='company',
question=_('Company'),
@@ -440,7 +440,7 @@ class QuestionList(ListView):
)
if self.request.event.settings.attendee_addresses_asked:
questions.append(
ctx['questions'].append(
FakeQuestion(
id='street',
question=_('Street'),
@@ -450,7 +450,7 @@ class QuestionList(ListView):
required=self.request.event.settings.attendee_addresses_required,
)
)
questions.append(
ctx['questions'].append(
FakeQuestion(
id='zipcode',
question=_('ZIP code'),
@@ -460,7 +460,7 @@ class QuestionList(ListView):
required=self.request.event.settings.attendee_addresses_required,
)
)
questions.append(
ctx['questions'].append(
FakeQuestion(
id='city',
question=_('City'),
@@ -470,7 +470,7 @@ class QuestionList(ListView):
required=self.request.event.settings.attendee_addresses_required,
)
)
questions.append(
ctx['questions'].append(
FakeQuestion(
id='country',
question=_('Country'),
@@ -481,9 +481,7 @@ class QuestionList(ListView):
)
)
questions += list(ctx['questions'])
questions.sort(key=lambda q: q.position)
ctx['questions'] = questions
ctx['questions'].sort(key=lambda q: q.position)
return ctx

View File

@@ -1177,19 +1177,6 @@ class OrderTransition(OrderView):
to = self.request.POST.get('status', '')
if self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and to == 'p' and self.mark_paid_form.is_valid():
ps = self.mark_paid_form.cleaned_data['amount']
if ps == Decimal('0.00') and self.order.pending_sum <= Decimal('0.00'):
p = self.order.payments.filter(state=OrderPayment.PAYMENT_STATE_CONFIRMED).last()
if p:
p._mark_order_paid(
user=self.request.user,
send_mail=self.mark_paid_form.cleaned_data['send_email'],
force=self.mark_paid_form.cleaned_data.get('force', False),
payment_refund_sum=self.order.payment_refund_sum,
)
messages.success(self.request, _('The order has been marked as paid.'))
return redirect(self.get_order_url())
try:
p = self.order.payments.get(
state__in=(OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED),

View File

@@ -226,7 +226,6 @@ class UserSettings(UpdateView):
msgs = []
if 'new_pw' in form.changed_data:
self.request.user.needs_password_change = False
msgs.append(_('Your password has been changed.'))
if 'email' in form.changed_data:
@@ -244,8 +243,6 @@ class UserSettings(UpdateView):
return sup
def get_success_url(self):
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
return self.request.GET.get("next")
return reverse('control:user.settings')

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -199,7 +199,8 @@ msgstr "تذاكر سارية المفعول"
msgid "Currently inside"
msgstr "حاليا بالداخل"
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -194,7 +194,8 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
msgid "close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2021-10-18 19:00+0000\n"
"Last-Translator: Tony Pavlik <kontakt@playton.cz>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -194,7 +194,8 @@ msgstr "Platné vstupenky"
msgid "Currently inside"
msgstr "Právě uvnitř"
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
msgid "close"
msgstr "zavřít"
@@ -509,10 +510,8 @@ msgstr "Nákupní košík vypršel"
#: 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] ""
"Produkty v nákupním košíku jsou pro vás rezervovány na jednu minutu."
msgstr[1] ""
"Produkty v nákupním košíku jsou pro vás rezervovány na {num} minuty."
msgstr[0] "Produkty v nákupním košíku jsou pro vás rezervovány na jednu minutu."
msgstr[1] "Produkty v nákupním košíku jsou pro vás rezervovány na {num} minuty."
msgstr[2] ""
"Produkty v nákupním košíku jsou pro vás rezervovány na dalších {num} minut."

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2021-09-13 09:48+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -206,7 +206,8 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2021-10-25 15:40+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -194,7 +194,8 @@ msgstr "Gültige Tickets"
msgid "Currently inside"
msgstr "Derzeit anwesend"
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
msgid "close"
msgstr "schließen"

View File

@@ -97,7 +97,6 @@ Explorer
FA
Favicon
Footer
Galicisch
gehostete
geht's
GENEXAMPLE
@@ -262,7 +261,6 @@ txt
ÜBERZAHLT
überzahlte
uhrzeit
UID
ungespeicherte
unkategorisiert
Unkategorisierte

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2021-10-25 15:40+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
@@ -194,7 +194,8 @@ msgstr "Gültige Tickets"
msgid "Currently inside"
msgstr "Derzeit anwesend"
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
msgid "close"
msgstr "schließen"

View File

@@ -97,7 +97,6 @@ Explorer
FA
Favicon
Footer
Galicisch
gehostete
geht's
GENEXAMPLE
@@ -262,7 +261,6 @@ txt
ÜBERZAHLT
überzahlte
uhrzeit
UID
ungespeicherte
unkategorisiert
Unkategorisierte

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+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"
@@ -193,7 +193,8 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
msgid "close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -209,7 +209,8 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2021-09-06 09:00+0000\n"
"Last-Translator: rauxenz <noquedandireccionesdisponibles@gmail.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -194,7 +194,8 @@ msgstr "Tickets válidos"
msgid "Currently inside"
msgstr "Actualmente adentro"
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,11 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2021-10-04 21:00+0000\n"
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/"
"pretix-js/fi/>\n"
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/fi/>\n"
"Language: fi\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -203,7 +203,8 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
@@ -497,12 +498,12 @@ msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:43
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle. Voit silti "
"suorittaa tilauksen loppuun niin kauan kuin tuotteita on saatavilla."
msgstr "Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle."
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2021-10-01 23:00+0000\n"
"Last-Translator: Fabian Rodriguez <magicfab@legoutdulibre.com>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -206,7 +206,8 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"

File diff suppressed because it is too large Load Diff

View File

@@ -1,795 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2021-11-04 07:00+0000\n"
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/"
"pretix-js/gl/>\n"
"Language: gl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.8\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
msgid "Marked as paid"
msgstr "Marcado como pagado"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
msgid "Comment:"
msgstr "Comentario:"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Placed orders"
msgstr "Ordes enviadas"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Paid orders"
msgstr "Ordes pagadas"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
msgid "Total revenue"
msgstr "Ingresos totais"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:12
msgid "Contacting Stripe …"
msgstr "Contactando con Stripe…"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
msgid "Total"
msgstr "Total"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr "Confirmando o pagamento…"
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
msgid "Contacting your bank …"
msgstr "Contactando co banco…"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:30
msgid "Select a check-in list"
msgstr "Seleccion unha lista de rexistro"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31
msgid "No active check-in lists found."
msgstr "Non se atoparon listas de rexistro activas."
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32
msgid "Switch check-in list"
msgstr "Cambiar lista de rexistro"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33
msgid "Search results"
msgstr "Resultados da procura"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34
msgid "No tickets found"
msgstr "Non se atoparon tickets"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:35
msgid "Result"
msgstr "Resultado"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:36
msgid "This ticket requires special attention"
msgstr "Este ticket require atención especial"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37
msgid "Switch direction"
msgstr "Cambiar dirección"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:38
msgid "Entry"
msgstr "Ingreso"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:39
msgid "Exit"
msgstr "Saída"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:40
msgid "Scan a ticket or search and press return…"
msgstr "Escanee o ticket ou busque e presione volver…"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:41
msgid "Load more"
msgstr "Cargar máis"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:42
msgid "Valid"
msgstr "Válido"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:43
msgid "Unpaid"
msgstr "Sen pagar"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:44
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:45
msgid "Canceled"
msgstr "Cancelado"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
msgid "Redeemed"
msgstr "Trocado"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Cancel"
msgstr "Cancelar"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr "Continuar"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Ticket not paid"
msgstr "Ticket pendente de pago"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr "Este ticket aínda non se pagou ¿Desexa continuar de todos modos?"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
msgid "Additional information required"
msgstr "Requírese de información adicional"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "Valid ticket"
msgstr "Ticket válido"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Exit recorded"
msgstr "Saída rexistrada"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
msgid "Ticket already used"
msgstr "Este ticket xa foi utilizado"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Information required"
msgstr "Información requerida"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Unknown ticket"
msgstr "Non se atopou ticket"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Ticket type not allowed here"
msgstr "Tipo de ticket non permitido"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Entry not allowed"
msgstr "Entrada non permitida"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket code revoked/changed"
msgstr "Código de ticket revocado/cambiado"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Order canceled"
msgstr "Orde cancelada"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Checked-in Tickets"
msgstr "Rexistro de código QR"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
msgid "Valid Tickets"
msgstr "Tickets válidos"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Currently inside"
msgstr "Actualmente dentro"
#: pretix/static/lightbox/js/lightbox.js:96
msgid "close"
msgstr "cerrar"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
msgid ""
"Your request is currently being processed. Depending on the size of your "
"event, this might take up to a few minutes."
msgstr ""
"A sua solicitude estase procesando. Isto pode tardar varios minutos, "
"dependendo do tamaño do seu evento."
#: pretix/static/pretixbase/js/asynctask.js:48
#: pretix/static/pretixbase/js/asynctask.js:124
msgid "Your request has been queued on the server and will soon be processed."
msgstr "A sua solicitude foi enviada ao servidor e será procesada en breve."
#: pretix/static/pretixbase/js/asynctask.js:54
#: pretix/static/pretixbase/js/asynctask.js:130
msgid ""
"Your request arrived on the server but we still wait for it to be processed. "
"If this takes longer than two minutes, please contact us or go back in your "
"browser and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:89
#: pretix/static/pretixbase/js/asynctask.js:175
#: pretix/static/pretixbase/js/asynctask.js:180
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr "Ocurreu un error de tipo {code}."
#: pretix/static/pretixbase/js/asynctask.js:92
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
"{code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:144
#: pretix/static/pretixcontrol/js/ui/mail.js:21
msgid "The request took too long. Please try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:183
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:205
msgid "We are processing your request …"
msgstr "Estamos procesando a sua solicitude…"
#: pretix/static/pretixbase/js/asynctask.js:213
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "Cerrar mensaxe"
#: pretix/static/pretixcontrol/js/clipboard.js:23
msgid "Copied!"
msgstr "¡Copiado!"
#: pretix/static/pretixcontrol/js/clipboard.js:29
msgid "Press Ctrl-C to copy!"
msgstr "¡Presione Control+C para copiar!"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:10
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:16
msgid "is one of"
msgstr "é un de"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:22
msgid "is before"
msgstr "está antes"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:26
msgid "is after"
msgstr "está despois"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:59
msgid "Product"
msgstr "Producto"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:63
msgid "Product variation"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:67
msgid "Current date and time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:71
msgid "Number of previous entries"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:75
msgid "Number of previous entries since midnight"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:79
msgid "Number of days with a previous entry"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:97
msgid "All of the conditions below (AND)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:98
msgid "At least one of the conditions below (OR)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
msgid "Event start"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
msgid "Event end"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
msgid "Event admission"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
msgid "custom date and time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
msgid "custom time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
msgid "Tolerance (minutes)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
msgid "Add condition"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
msgid "minutes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:69
msgid "Lead Scan QR"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:71
msgid "Check-in QR"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:313
msgid "The PDF background file could not be loaded for the following reason:"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:521
msgid "Group of objects"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:527
msgid "Text object"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:529
msgid "Barcode area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:531
msgid "Image area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:533
msgid "Powered by pretix"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:535
msgid "Object"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:539
msgid "Ticket design"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:813
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:862
#: pretix/static/pretixcontrol/js/ui/editor.js:901
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:886
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:19
msgid "An error has occurred."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:54
msgid "Generating messages …"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:70
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:271
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:275
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:279
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:417
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:418
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:419
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:422
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:834
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:870
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:911
msgid "You have unsaved changes!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/orderchange.js:25
msgid "Calculating default price…"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:82
msgid "Count"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:136
#: pretix/static/pretixpresale/js/ui/questions.js:269
msgid "Yes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:269
msgid "No"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/subevent.js:111
msgid "(one more date)"
msgid_plural "({num} more dates)"
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:43
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
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] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/main.js:144
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:168
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "Register"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
msgctxt "widget"
msgid ""
"There are currently a lot of users in this ticket shop. Please open the shop "
"in a new tab to continue."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid "Open ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
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 ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgctxt "widget"
msgid "See variations"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgctxt "widget"
msgid "Choose a different date"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgctxt "widget"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgctxt "widget"
msgid "Next month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgctxt "widget"
msgid "Previous month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgctxt "widget"
msgid "Next week"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgctxt "widget"
msgid "Previous week"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
msgctxt "widget"
msgid "Open seat selection"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:56
msgctxt "widget"
msgid "Load more"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "Tu"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "We"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "Th"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "Fr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "Sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "February"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:69
msgid "March"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:70
msgid "April"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:71
msgid "May"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:72
msgid "June"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:73
msgid "July"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:74
msgid "August"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:75
msgid "September"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:76
msgid "October"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:77
msgid "November"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:78
msgid "December"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2021-09-24 13:54+0000\n"
"Last-Translator: ofirtro <ofir.tro@gmail.com>\n"
"Language-Team: Hebrew <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -195,7 +195,8 @@ msgstr "כרטיסים תקפים"
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
msgid "close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2020-01-24 08:00+0000\n"
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -207,7 +207,8 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2021-07-19 11:57+0000\n"
"Last-Translator: dedecosta <dedecosta2@live.it>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -194,7 +194,8 @@ msgstr "Biglietti validi"
msgid "Currently inside"
msgstr "Attualmente all'interno"
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2021-10-11 15:56+0000\n"
"Last-Translator: DJG Bayern <pretix2021weblate@aruki.de>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -194,7 +194,8 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
msgid "close"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2019-11-13 06:00+0000\n"
"Last-Translator: Zane Smite <z.smite@riga-jurmala.com>\n"
"Language-Team: Latvian <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -208,7 +208,8 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"POT-Creation-Date: 2021-10-17 14:57+0000\n"
"PO-Revision-Date: 2021-05-31 11:26+0000\n"
"Last-Translator: zackern <zacker@zacker.no>\n"
"Language-Team: Norwegian Bokmål <https://translate.pretix.eu/projects/pretix/"
@@ -196,7 +196,8 @@ msgstr "Gydlige billetter"
msgid "Currently inside"
msgstr "Inne nå"
#: pretix/static/lightbox/js/lightbox.js:96
#: pretix/static/lightbox/js/lightbox.js:90
#: pretix/static/lightbox/js/lightbox.min.js:13
msgid "close"
msgstr ""

File diff suppressed because it is too large Load Diff

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