Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
e2dad8fdcc Database consistency checks 2021-10-29 12:02:00 +02:00
190 changed files with 61666 additions and 87519 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.2"
__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

@@ -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

@@ -33,6 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
import io
import re
import tempfile
from collections import OrderedDict, namedtuple
from decimal import Decimal
@@ -45,13 +46,26 @@ from django.conf import settings
from django.db.models import QuerySet
from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, KNOWN_TYPES, Cell
from pretix.base.models import Event
from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for plugins using excel_safe
SafeWorkbook, remove_invalid_excel_chars as excel_safe,
)
__ = excel_safe # just so the compatbility import above is "used" and doesn't get removed by linter
def excel_safe(val):
if isinstance(val, Cell):
return val
if not isinstance(val, KNOWN_TYPES):
val = str(val)
if isinstance(val, bytes):
val = val.decode("utf-8", errors="ignore")
if isinstance(val, str):
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
return val
class BaseExporter:
@@ -214,7 +228,7 @@ class ListExporter(BaseExporter):
pass
def _render_xlsx(self, form_data, output_file=None):
wb = SafeWorkbook(write_only=True)
wb = Workbook(write_only=True)
ws = wb.create_sheet()
self.prepare_xlsx_sheet(ws)
try:
@@ -228,7 +242,7 @@ class ListExporter(BaseExporter):
total = line.total
continue
ws.append([
val for val in line
excel_safe(val) for val in line
])
if total:
counter += 1
@@ -333,7 +347,7 @@ class MultiSheetListExporter(ListExporter):
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_xlsx(self, form_data, output_file=None):
wb = SafeWorkbook(write_only=True)
wb = Workbook(write_only=True)
n_sheets = len(self.sheets)
for i_sheet, (s, l) in enumerate(self.sheets):
ws = wb.create_sheet(str(l))
@@ -347,7 +361,8 @@ class MultiSheetListExporter(ListExporter):
total = line.total
continue
ws.append([
val for val in line
excel_safe(val)
for val in line
])
if total:
counter += 1

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

@@ -674,7 +674,7 @@ class BaseQuestionsForm(forms.Form):
label=label, required=required,
min_value=q.valid_number_min or Decimal('0.00'),
max_value=q.valid_number_max,
help_text=help_text,
help_text=q.help_text,
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_STRING:

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

@@ -0,0 +1,54 @@
#
# 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/>.
#
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
# 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: 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 django.db import models
from django.utils.translation import gettext_lazy as _
from pretix.base.models import LoggedModel
class Check(LoggedModel):
RESULT_OK = 'ok'
RESULT_WARNING = 'warning'
RESULT_ERROR = 'error'
RESULTS = (
(RESULT_OK, _('OK')),
(RESULT_WARNING, _('Warning')),
(RESULT_ERROR, _('Error')),
)
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

@@ -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,
@@ -1994,7 +1974,7 @@ Your {organizer} team"""))
),
},
'theme_color_success': {
'default': '#50a167',
'default': '#50A167',
'type': str,
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
@@ -2016,7 +1996,7 @@ Your {organizer} team"""))
),
},
'theme_color_danger': {
'default': '#c44f4f',
'default': '#C44F4F',
'type': str,
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
@@ -2038,7 +2018,7 @@ Your {organizer} team"""))
),
},
'theme_color_background': {
'default': '#f5f5f5',
'default': '#FFFFFF',
'type': str,
'form_class': forms.CharField,
'serializer_class': serializers.CharField,
@@ -2464,7 +2444,7 @@ Your {organizer} team"""))
)
},
'name_scheme': {
'default': 'full', # default for new events is 'given_family'
'default': 'full',
'type': str
},
'giftcard_length': {

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,26 +0,0 @@
{% extends "error.html" %}
{% load i18n %}
{% load rich_text %}
{% load static %}
{% block title %}{% trans "Redirect" %}{% endblock %}
{% block content %}
<i class="fa fa-link fa-fw big-icon"></i>
<div class="error-details">
<h1>{% trans "Redirect" %}</h1>
<h3>
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %}
The link you clicked on wants to redirect you to a destination on the website {{ host }}.
{% endblocktrans %}
{% blocktrans trimmed %}
Please only proceed if you trust this website to be safe.
{% endblocktrans %}
</h3>
<p>
<a href="{{ url }}" class="btn btn-primary btn-lg">
{% blocktrans trimmed with host=hostname %}
Proceed to {{ host }}
{% endblocktrans %}
</a>
</p>
</div>
{% endblock %}

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

@@ -24,21 +24,6 @@ import urllib.parse
from django.core import signing
from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.urls import reverse
from django.shortcuts import render
def _is_samesite_referer(request):
referer = request.META.get('HTTP_REFERER')
if referer is None:
return False
referer = urllib.parse.urlparse(referer)
# Make sure we have a valid URL for Referer.
if '' in (referer.scheme, referer.netloc):
return False
return (referer.scheme, referer.netloc) == (request.scheme, request.get_host())
def redir_view(request):
@@ -47,14 +32,6 @@ def redir_view(request):
url = signer.unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
if not _is_samesite_referer(request):
u = urllib.parse.urlparse(url)
return render(request, 'pretixbase/redirect.html', {
'hostname': u.hostname,
'url': url,
})
r = HttpResponseRedirect(url)
r['X-Robots-Tag'] = 'noindex'
return r

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

@@ -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

@@ -74,17 +74,17 @@
{{ c.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if c.type == "exit" %}
{% if c.auto_checked_in %}
<span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip"
<span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip_html"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
{% endif %}
{% elif c.forced and c.successful %}
<span class="fa fa-fw fa-warning" data-toggle="tooltip"
<span class="fa fa-fw fa-warning" data-toggle="tooltip_html"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}"></span>
{% elif c.forced and not c.successful %}
<br>
<small class="text-muted">{% trans "Failed in offline mode" %}</small>
{% elif c.auto_checked_in %}
<span class="fa fa-fw fa-magic" data-toggle="tooltip"
<span class="fa fa-fw fa-magic" data-toggle="tooltip_html"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}"></span>
{% endif %}
</td>

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

@@ -1,6 +1,6 @@
{% load i18n %}
<div class="quotabox availability" data-toggle="tooltip_html" data-placement="top"
title="{% trans "Quota:" %} {{ q.name|force_escape|force_escape }}<br>{% blocktrans with date=q.cached_availability_time|date:"SHORT_DATETIME_FORMAT" %}Numbers as of {{ date }}{% endblocktrans %}">
title="{% trans "Quota:" %} {{ q.name }}<br>{% blocktrans with date=q.cached_availability_time|date:"SHORT_DATETIME_FORMAT" %}Numbers as of {{ date }}{% endblocktrans %}">
{% if q.size|default_if_none:"NONE" == "NONE" %}
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-100">

View File

@@ -1,6 +1,6 @@
{% load i18n %}
<a class="quotabox" data-toggle="tooltip_html" data-placement="top"
title="{% trans "Quota:" %} {{ q.name|force_escape|force_escape }}{% if q.cached_avail.1 is not None %}<br>{% blocktrans with num=q.cached_avail.1 %}Currently available: {{ num }}{% endblocktrans %}{% endif %}"
title="{% trans "Quota:" %} {{ q.name }}{% if q.cached_avail.1 is not None %}<br>{% blocktrans with num=q.cached_avail.1 %}Currently available: {{ num }}{% endblocktrans %}{% endif %}"
href="{% url "control:event.items.quotas.show" event=q.event.slug organizer=q.event.organizer.slug quota=q.pk %}">
{% if q.size|default_if_none:"NONE" == "NONE" %}
<div class="progress">

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

@@ -360,19 +360,19 @@
{% if line.checkins.all %}
{% for c in line.all_checkins.all %}
{% if not c.successful %}
<span class="fa fa-fw fa-exclamation-circle text-danger" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Denied scan: {{ date }}{% endblocktrans %}<br>{{ c.get_error_reason_display }}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-exclamation-circle text-danger" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Denied scan: {{ date }}{% endblocktrans %}<br>{{ c.get_error_reason_display }}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% elif c.type == "exit" %}
{% if c.auto_checked_in %}
<span class="fa fa-fw text-success fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
<span class="fa fa-fw text-success fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
{% else %}
<span class="fa fa-fw text-success fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw text-success fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% endif %}
{% elif c.forced %}
<span class="fa fa-fw fa-warning text-warning" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-warning text-warning" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% elif c.auto_checked_in %}
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% else %}
<span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% endif %}
{% endfor %}
{% endif %}

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')

View File

@@ -1,113 +0,0 @@
import re
from inspect import isgenerator
from openpyxl import Workbook
from openpyxl.cell.cell import (
ILLEGAL_CHARACTERS_RE, KNOWN_TYPES, TIME_TYPES, TYPE_FORMULA, TYPE_STRING,
Cell,
)
from openpyxl.compat import NUMERIC_TYPES
from openpyxl.utils import column_index_from_string
from openpyxl.utils.exceptions import ReadOnlyWorkbookException
from openpyxl.worksheet._write_only import WriteOnlyWorksheet
from openpyxl.worksheet.worksheet import Worksheet
SAFE_TYPES = NUMERIC_TYPES + TIME_TYPES + (bool, type(None))
"""
This module provides a safer version of openpyxl's `Workbook` class to generate XLSX files from
user-generated data using `WriteOnlyWorksheet` and `ws.append()`. We commonly use these methods
to output e.g. order data, which contains data from untrusted sources such as attendee names.
There are mainly two problems this solves:
- It makes sure strings starting with = are treated as text, not as a formula, as openpyxl will
otherwise assume, which can be used for remote code execution.
- It removes characters considered invalid by Excel to avoid exporter crashes.
"""
def remove_invalid_excel_chars(val):
if isinstance(val, Cell):
return val
if not isinstance(val, KNOWN_TYPES):
val = str(val)
if isinstance(val, bytes):
val = val.decode("utf-8", errors="ignore")
if isinstance(val, str):
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
return val
def SafeCell(*args, value=None, **kwargs):
value = remove_invalid_excel_chars(value)
c = Cell(*args, value=value, **kwargs)
if c.data_type == TYPE_FORMULA:
c.data_type = TYPE_STRING
return c
class SafeAppendMixin:
def append(self, iterable):
row_idx = self._current_row + 1
if isinstance(iterable, (list, tuple, range)) or isgenerator(iterable):
for col_idx, content in enumerate(iterable, 1):
if isinstance(content, Cell):
# compatible with write-only mode
cell = content
if cell.parent and cell.parent != self:
raise ValueError("Cells cannot be copied from other worksheets")
cell.parent = self
cell.column = col_idx
cell.row = row_idx
else:
cell = SafeCell(self, row=row_idx, column=col_idx, value=remove_invalid_excel_chars(content))
self._cells[(row_idx, col_idx)] = cell
elif isinstance(iterable, dict):
for col_idx, content in iterable.items():
if isinstance(col_idx, str):
col_idx = column_index_from_string(col_idx)
cell = SafeCell(self, row=row_idx, column=col_idx, value=content)
self._cells[(row_idx, col_idx)] = cell
else:
self._invalid_row(iterable)
self._current_row = row_idx
class SafeWriteOnlyWorksheet(SafeAppendMixin, WriteOnlyWorksheet):
pass
class SafeWorksheet(SafeAppendMixin, Worksheet):
pass
class SafeWorkbook(Workbook):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self._sheets:
# monkeypatch existing sheets
for s in self._sheets:
s.append = SafeAppendMixin.append
def create_sheet(self, title=None, index=None):
if self.read_only:
raise ReadOnlyWorkbookException('Cannot create new sheet in a read-only workbook')
if self.write_only:
new_ws = SafeWriteOnlyWorksheet(parent=self, title=title)
else:
new_ws = SafeWorksheet(parent=self, title=title)
self._add_sheet(sheet=new_ws, index=index)
return new_ws

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-11-29 08:28+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"
@@ -535,20 +536,20 @@ msgstr "ستسترد %(currency)%(amount)"
msgid "Please enter the amount the organizer can keep."
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
#: pretix/static/pretixpresale/js/ui/main.js:339
#: 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:375
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr "مطلوب"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: 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:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
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-11-29 08:28+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 ""
@@ -510,22 +511,22 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:339
#: 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:375
#: pretix/static/pretixpresale/js/ui/main.js:371
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Cistella expirada"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: 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:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
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-11-29 08:28+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."
@@ -528,20 +527,20 @@ msgstr "Dostanete %(currency)s %(amount)s zpět"
msgid "Please enter the amount the organizer can keep."
msgstr "Zadejte částku, kterou si organizátor může ponechat."
#: pretix/static/pretixpresale/js/ui/main.js:339
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Zadejte prosím množství pro jeden z typů vstupenek."
#: pretix/static/pretixpresale/js/ui/main.js:375
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr "povinný"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Časové pásmo:"
#: pretix/static/pretixpresale/js/ui/main.js:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "Místní čas:"

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-11-29 08:28+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"
@@ -563,22 +564,22 @@ msgstr "fra %(currency)s %(price)s"
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:339
#: 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:375
#: pretix/static/pretixpresale/js/ui/main.js:371
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Kurv udløbet"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Tidszone:"
#: pretix/static/pretixpresale/js/ui/main.js:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "Din lokaltid:"

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-11-29 08:28+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"
@@ -532,20 +533,20 @@ msgstr "Sie erhalten %(currency)s %(amount)s zurück"
msgid "Please enter the amount the organizer can keep."
msgstr "Bitte geben Sie den Betrag ein, den der Veranstalter einbehalten darf."
#: pretix/static/pretixpresale/js/ui/main.js:339
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Bitte tragen Sie eine Menge für eines der Produkte ein."
#: pretix/static/pretixpresale/js/ui/main.js:375
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr "verpflichtend"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Zeitzone:"
#: pretix/static/pretixpresale/js/ui/main.js:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "Deine lokale Zeit:"

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-11-29 08:28+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"
@@ -531,20 +532,20 @@ msgstr "Du erhältst %(currency)s %(amount)s zurück"
msgid "Please enter the amount the organizer can keep."
msgstr "Bitte gib den Betrag ein, den der Veranstalter einbehalten darf."
#: pretix/static/pretixpresale/js/ui/main.js:339
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Bitte trage eine Menge für eines der Produkte ein."
#: pretix/static/pretixpresale/js/ui/main.js:375
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr "verpflichtend"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Zeitzone:"
#: pretix/static/pretixpresale/js/ui/main.js:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "Deine lokale Zeit:"

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-11-29 08:28+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 ""
@@ -505,20 +506,20 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:339
#: 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:375
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: 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:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
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-11-29 08:28+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"
@@ -575,22 +576,22 @@ msgstr "απο %(currency)s %(price)s"
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:339
#: 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:375
#: pretix/static/pretixpresale/js/ui/main.js:371
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Το καλάθι έληξε"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: 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:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,9 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-29 08:28+0000\n"
"PO-Revision-Date: 2021-11-25 21:00+0000\n"
"Last-Translator: Ismael Menéndez Fernández <ismael.menendez@balidea.com>\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-"
"js/es/>\n"
"Language: es\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.8\n"
"X-Generator: Weblate 4.6\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -194,7 +194,11 @@ 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"
msgid "close"
msgstr "Cerrar"
@@ -498,22 +502,26 @@ msgstr[0] "(una fecha más)"
msgstr[1] "({num} más fechas)"
#: 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 ""
"Los elementos en su carrito de compras ya no se encuentran reservados. "
"Puedes seguir añadiendo más productos mientras estén disponibles."
msgstr "Los elementos en su carrito de compras ya no se encuentran reservados."
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "El carrito de compras ha expirado"
#: pretix/static/pretixpresale/js/ui/cart.js:50
#, fuzzy
#| 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."
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] ""
"Los elementos en su carrito de compras se han reservado por {num} minutos."
"Los elementos en su carrito de compras se han reservado por un minuto."
msgstr[1] ""
"Los elementos en su carrito de compras se han reservado por {num} minutos."
@@ -529,20 +537,20 @@ msgstr "Obtienes %(currency)s %(price)s de vuelta"
msgid "Please enter the amount the organizer can keep."
msgstr "Por favor, ingrese el monto que el organizador puede quedarse."
#: pretix/static/pretixpresale/js/ui/main.js:339
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Por favor, introduzca un valor para cada tipo de entrada."
#: pretix/static/pretixpresale/js/ui/main.js:375
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr "campo requerido"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Zona horaria:"
#: pretix/static/pretixpresale/js/ui/main.js:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "Su hora local:"

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-29 08:28+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"
@@ -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"
@@ -528,22 +529,22 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:339
#: 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:375
#: pretix/static/pretixpresale/js/ui/main.js:371
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Ostoskori on vanhentunut"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Aikavyöhyke:"
#: pretix/static/pretixpresale/js/ui/main.js:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""

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-11-29 08:28+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"
@@ -567,22 +568,22 @@ msgstr "de %(currency)s %(price)s"
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:339
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "SVP entrez une quantité pour un de vos types de billets."
#: pretix/static/pretixpresale/js/ui/main.js:375
#: pretix/static/pretixpresale/js/ui/main.js:371
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Panier expiré"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: 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:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -1,820 +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-11-29 08:28+0000\n"
"PO-Revision-Date: 2021-11-25 21: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 "Pedidos enviados"
#: 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 "Seleccione 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 a 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 tíckets"
#: 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 tícket require atención especial"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37
msgid "Switch direction"
msgstr "Cambiar enderezo"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:38
msgid "Entry"
msgstr "Acceso"
#: 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 tícket 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 "Tícket 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 tícket aínda non se pagou. Desexa continuar de todos os xeitos?"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
msgid "Additional information required"
msgstr "Requírese información adicional"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "Valid ticket"
msgstr "Entrada válida"
#: 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 tícket xa foi utilizado"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Information required"
msgstr "Información requirida"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Unknown ticket"
msgstr "Tícket descoñecido"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Ticket type not allowed here"
msgstr "Tipo de tícket 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 tícket revogado/cambiado"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Order canceled"
msgstr "Pedido cancelado"
#: 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 "Tíckets 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 súa 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 súa 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 ""
"A súa solicitude chegou ao servidor pero seguimos esperando a que sexa "
"procesada. Se tarda máis de dous minutos, por favor, contacte con nós ou "
"volva á páxina anterior no seu navegador e inténteo de novo."
#: 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 ""
"Agora mesmo non podemos contactar co servidor, pero seguímolo intentando. O "
"último código de erro foi: {code}"
#: 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 "A petición levou demasiado tempo. Inténteo de novo."
#: 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 ""
"Agora mesmo non podemos contactar co servidor. Por favor, inténteo de novo. "
"Código de erro: {code}"
#: pretix/static/pretixbase/js/asynctask.js:205
msgid "We are processing your request …"
msgstr "Estamos procesando a súa 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 ""
"Estamos enviando a súa solicitude ao servidor. Se este proceso tarda máis "
"dun minuto, por favor, revise a súa conexión a Internet, recargue a páxina e "
"inténteo de novo."
#: 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 "Produto"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:63
msgid "Product variation"
msgstr "Ver variacións do produto"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:67
msgid "Current date and time"
msgstr "Data e hora actual"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:71
msgid "Number of previous entries"
msgstr "Número de entradas previas"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:75
msgid "Number of previous entries since midnight"
msgstr "Número de entradas previas desde a medianoite"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:79
msgid "Number of days with a previous entry"
msgstr "Número de días cunha entrada previa"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:97
msgid "All of the conditions below (AND)"
msgstr "Todas as condicións seguintes (E)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:98
msgid "At least one of the conditions below (OR)"
msgstr "Polo menos unha das seguintes condicións (OU)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
msgid "Event start"
msgstr "Comezo do evento"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
msgid "Event end"
msgstr "Fin do evento"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
msgid "Event admission"
msgstr "Admisión ao evento"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
msgid "custom date and time"
msgstr "Seleccionar data e hora"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
msgid "custom time"
msgstr "Seleccionar hora"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
msgid "Tolerance (minutes)"
msgstr "Tolerancia (en minutos)"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
msgid "Add condition"
msgstr "Engadir condición"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
msgid "minutes"
msgstr "minutos"
#: pretix/static/pretixcontrol/js/ui/editor.js:69
msgid "Lead Scan QR"
msgstr "Escanear QR de clientela potencial"
#: pretix/static/pretixcontrol/js/ui/editor.js:71
msgid "Check-in QR"
msgstr "QR de validación"
#: pretix/static/pretixcontrol/js/ui/editor.js:313
msgid "The PDF background file could not be loaded for the following reason:"
msgstr "O arquivo PDF de fondo non se puido cargar polo motivo seguinte:"
#: pretix/static/pretixcontrol/js/ui/editor.js:521
msgid "Group of objects"
msgstr "Grupo de obxectos"
#: pretix/static/pretixcontrol/js/ui/editor.js:527
msgid "Text object"
msgstr "Obxecto de texto"
#: pretix/static/pretixcontrol/js/ui/editor.js:529
msgid "Barcode area"
msgstr "Área para código de barras"
#: pretix/static/pretixcontrol/js/ui/editor.js:531
msgid "Image area"
msgstr "Área de imaxe"
#: pretix/static/pretixcontrol/js/ui/editor.js:533
msgid "Powered by pretix"
msgstr "Desenvolto por Pretix"
#: pretix/static/pretixcontrol/js/ui/editor.js:535
msgid "Object"
msgstr "Obxecto"
#: pretix/static/pretixcontrol/js/ui/editor.js:539
msgid "Ticket design"
msgstr "Deseño do tícket"
#: pretix/static/pretixcontrol/js/ui/editor.js:813
msgid "Saving failed."
msgstr "O gardado fallou."
#: 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 ""
"Houbo un erro mentres se cargaba o arquivo PDF. Por favor, inténteo de novo."
#: pretix/static/pretixcontrol/js/ui/editor.js:886
msgid "Do you really want to leave the editor without saving your changes?"
msgstr "Realmente desexa saír do editor sen gardar os cambios?"
#: pretix/static/pretixcontrol/js/ui/mail.js:19
msgid "An error has occurred."
msgstr "Houbo un erro."
#: pretix/static/pretixcontrol/js/ui/mail.js:54
msgid "Generating messages …"
msgstr "Xerando mensaxes…"
#: pretix/static/pretixcontrol/js/ui/main.js:70
msgid "Unknown error."
msgstr "Erro descoñecido."
#: pretix/static/pretixcontrol/js/ui/main.js:271
msgid "Your color has great contrast and is very easy to read!"
msgstr "A túa cor ten moito contraste e é moi doada de ler!"
#: pretix/static/pretixcontrol/js/ui/main.js:275
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
"A túa cor ten un contraste axeitado e probablemente sexa suficientemente "
"lexible!"
#: 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 ""
"A túa cor ten mal contraste para un texto con fondo branco. Por favor, "
"escolle un ton máis escuro."
#: pretix/static/pretixcontrol/js/ui/main.js:417
msgid "All"
msgstr "Todos"
#: pretix/static/pretixcontrol/js/ui/main.js:418
msgid "None"
msgstr "Ningún"
#: pretix/static/pretixcontrol/js/ui/main.js:419
msgid "Search query"
msgstr "Consultar unha procura"
#: pretix/static/pretixcontrol/js/ui/main.js:422
msgid "Selected only"
msgstr "Soamente seleccionados"
#: pretix/static/pretixcontrol/js/ui/main.js:834
msgid "Use a different name internally"
msgstr "Usar un nome diferente internamente"
#: pretix/static/pretixcontrol/js/ui/main.js:870
msgid "Click to close"
msgstr "Click para cerrar"
#: pretix/static/pretixcontrol/js/ui/main.js:911
msgid "You have unsaved changes!"
msgstr "Tes cambios sen gardar!"
#: pretix/static/pretixcontrol/js/ui/orderchange.js:25
msgid "Calculating default price…"
msgstr "Calculando o prezo por defecto…"
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr "Outros"
#: pretix/static/pretixcontrol/js/ui/question.js:82
msgid "Count"
msgstr "Cantidade"
#: pretix/static/pretixcontrol/js/ui/question.js:136
#: pretix/static/pretixpresale/js/ui/questions.js:269
msgid "Yes"
msgstr "Si"
#: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:269
msgid "No"
msgstr "Non"
#: pretix/static/pretixcontrol/js/ui/subevent.js:111
msgid "(one more date)"
msgid_plural "({num} more dates)"
msgstr[0] "(unha data máis)"
msgstr[1] "({num} máis datas)"
#: 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 ""
"Os artigos da túa cesta xa non están reservados para ti. Aínda podes "
"completar o teu pedido mentres estean dispoñibles."
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "O carro da compra caducou"
#: 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] "Os artigos da túa cesta están reservados para ti durante un minuto."
msgstr[1] ""
"Os artigos da túa cesta están reservados para ti durante {num} minutos."
#: pretix/static/pretixpresale/js/ui/main.js:144
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "O organizador queda %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "You get %(currency)s %(amount)s back"
msgstr "Obtés %(currency)s %(price)s de volta"
#: pretix/static/pretixpresale/js/ui/main.js:168
msgid "Please enter the amount the organizer can keep."
msgstr "Por favor, ingrese a cantidade que pode conservar o organizador."
#: pretix/static/pretixpresale/js/ui/main.js:339
msgid "Please enter a quantity for one of the ticket types."
msgstr "Por favor, introduza un valor para cada tipo de entrada."
#: pretix/static/pretixpresale/js/ui/main.js:375
msgid "required"
msgstr "campo requirido"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
msgid "Time zone:"
msgstr "Zona horaria:"
#: pretix/static/pretixpresale/js/ui/main.js:488
msgid "Your local time:"
msgstr "A súa hora local:"
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Sold out"
msgstr "Esgotado"
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "Buy"
msgstr "Mercar"
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "Register"
msgstr "Rexistrarse"
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "Reserved"
msgstr "Reservado"
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "FREE"
msgstr "De balde"
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr "dende %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr "inclúe %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:24
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr "máis %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "incl. taxes"
msgstr "impostos incluídos"
#: pretix/static/pretixpresale/js/widget/widget.js:26
msgctxt "widget"
msgid "plus taxes"
msgstr "máis impostos"
#: pretix/static/pretixpresale/js/widget/widget.js:27
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr "dispoñible actualmente: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "Only available with a voucher"
msgstr "Só dispoñible mediante vale"
#: pretix/static/pretixpresale/js/widget/widget.js:29
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr "cantidade mínima de pedido: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Close ticket shop"
msgstr "Cerrar a tenda de tíckets"
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr "Non se puido cargar a tenda de tíckets."
#: 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 ""
"Actualmente hai moitas persoas usuarias na tenda de tíckets. Por favor, abra "
"a tenda nunha nova pestana para continuar."
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid "Open ticket shop"
msgstr "Abrir a tenda de tíckets"
#: pretix/static/pretixpresale/js/widget/widget.js:35
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
"O carro de compras non se puido crear. Por favor, inténteo de novo máis tarde"
#: 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 ""
"Non puidemos crear o seu carro debido a que hai moitas persoas usuarias na "
"tenda. Por favor, presione \"Continuar\" para intentalo nunha nova pestana."
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Waiting list"
msgstr "Lista de agarda"
#: 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 ""
"Xa ten un carro de compras activo para este evento. Se selecciona máis "
"produtos, estes engadiranse ao carro actual."
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "Resume checkout"
msgstr "Continuar co pagamento"
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Redeem a voucher"
msgstr "Trocar un vale"
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Redeem"
msgstr "Trocar"
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Voucher code"
msgstr "Código do cupón"
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Close"
msgstr "Cerrar"
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgctxt "widget"
msgid "Continue"
msgstr "Continuar"
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgctxt "widget"
msgid "See variations"
msgstr "Ver variacións"
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgctxt "widget"
msgid "Choose a different event"
msgstr "Elixir un evento distinto"
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgctxt "widget"
msgid "Choose a different date"
msgstr "Elixir unha data diferente"
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgctxt "widget"
msgid "Back"
msgstr "Atrás"
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgctxt "widget"
msgid "Next month"
msgstr "Mes seguinte"
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgctxt "widget"
msgid "Previous month"
msgstr "Mes anterior"
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgctxt "widget"
msgid "Next week"
msgstr "Semana seguinte"
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgctxt "widget"
msgid "Previous week"
msgstr "Semana anterior"
#: pretix/static/pretixpresale/js/widget/widget.js:55
msgctxt "widget"
msgid "Open seat selection"
msgstr "Abrir selección de asentos"
#: pretix/static/pretixpresale/js/widget/widget.js:56
msgctxt "widget"
msgid "Load more"
msgstr "Cargar máis"
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "Mo"
msgstr "Lu"
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "Tu"
msgstr "Mar"
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "We"
msgstr "Mér"
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "Th"
msgstr "Xo"
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "Fr"
msgstr "Ve"
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "Sa"
msgstr "Sáb"
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "Su"
msgstr "Dom"
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "January"
msgstr "Xaneiro"
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "February"
msgstr "Febreiro"
#: pretix/static/pretixpresale/js/widget/widget.js:69
msgid "March"
msgstr "Marzo"
#: pretix/static/pretixpresale/js/widget/widget.js:70
msgid "April"
msgstr "Abril"
#: pretix/static/pretixpresale/js/widget/widget.js:71
msgid "May"
msgstr "Maio"
#: pretix/static/pretixpresale/js/widget/widget.js:72
msgid "June"
msgstr "Xuño"
#: pretix/static/pretixpresale/js/widget/widget.js:73
msgid "July"
msgstr "Xullo"
#: pretix/static/pretixpresale/js/widget/widget.js:74
msgid "August"
msgstr "Agosto"
#: pretix/static/pretixpresale/js/widget/widget.js:75
msgid "September"
msgstr "Setembro"
#: pretix/static/pretixpresale/js/widget/widget.js:76
msgid "October"
msgstr "Outubro"
#: pretix/static/pretixpresale/js/widget/widget.js:77
msgid "November"
msgstr "Novembro"
#: pretix/static/pretixpresale/js/widget/widget.js:78
msgid "December"
msgstr "Decembro"

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-11-29 08:28+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 ""
@@ -513,20 +514,20 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:339
#: 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:375
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: 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:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
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-11-29 08:28+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"
@@ -563,22 +564,22 @@ msgstr "%(currency) %(price)-tól"
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:339
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Adjon meg egy mennyiséget az egyik jegytípusból."
#: pretix/static/pretixpresale/js/ui/main.js:375
#: pretix/static/pretixpresale/js/ui/main.js:371
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "A kosár lejárt"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: 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:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,9 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-29 08:28+0000\n"
"PO-Revision-Date: 2021-11-20 03:00+0000\n"
"Last-Translator: Marco Giacopuzzi <marco.giaco2000@gmail.com>\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-"
"js/it/>\n"
"Language: it\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.8\n"
"X-Generator: Weblate 4.6\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -194,7 +194,11 @@ 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"
msgid "close"
msgstr "Chiudi"
@@ -493,18 +497,22 @@ msgstr[0] "(un'altra data)"
msgstr[1] "({num} altre date)"
#: 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 ""
"Gli articoli nel tuo carrello non sono più riservati per te. Puoi ancora "
"completare il tuo ordine finché sono disponibili."
msgstr "I prodotti nel tuo carrello non sono più disponibili."
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "Carrello scaduto"
#: pretix/static/pretixpresale/js/ui/cart.js:50
#, fuzzy
#| 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."
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] "Gli elementi nel tuo carrello sono riservati per 1 minuto."
@@ -522,20 +530,20 @@ msgstr "Ricevi indietro %(currency)s %(amount)s"
msgid "Please enter the amount the organizer can keep."
msgstr "Inserisci l'importo che l'organizzatore può trattenere."
#: pretix/static/pretixpresale/js/ui/main.js:339
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Inserisci la quantità per una tipologia di biglietto."
#: pretix/static/pretixpresale/js/ui/main.js:375
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr "richiesto"
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Fuso orario:"
#: pretix/static/pretixpresale/js/ui/main.js:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "Ora locale:"
@@ -735,9 +743,11 @@ msgid "Open seat selection"
msgstr "Apri la selezione dei posti"
#: pretix/static/pretixpresale/js/widget/widget.js:56
#, fuzzy
#| msgid "Load more"
msgctxt "widget"
msgid "Load more"
msgstr "Mostra di più"
msgstr "Carica di più"
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "Mo"

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-11-29 08:28+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 ""
@@ -504,20 +505,20 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:339
#: 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:375
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:478
#: pretix/static/pretixpresale/js/ui/main.js:497
#: 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:488
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""

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