Compare commits

...
Author SHA1 Message Date
Richard Schreiber 421c098ec2 Presale: make cart more compact 2023-02-07 18:10:37 +01:00
Raphael Michel e8785f4117 Check-in rule editor: Add duplicate option 2023-02-07 17:12:12 +01:00
Raphael Michel accb9f8b13 Check-in rule editor: Fix reactivity issue in select2 component 2023-02-07 17:12:12 +01:00
Phin WolkwitzandGitHub 98d290992c Docs: Add comment to webhook API (#3096) 2023-02-07 16:38:33 +01:00
Raphael Michel 9a56874083 Subevents: Validate time order of product availability 2023-02-07 15:29:11 +01:00
ser8phinandGitHub 82dd417a8e Webhooks: Add comment field (#3095) 2023-02-07 15:17:19 +01:00
Richard SchreiberandGitHub ba2c6e1e58 Event index: Increase white-space for products and headlines (#3092) 2023-02-07 15:03:28 +01:00
Thomas VrankenandRaphael Michel 38b8269f14 Translations: Update Dutch
Currently translated at 86.1% (4389 of 5093 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl/

powered by weblate
2023-02-07 15:03:19 +01:00
Raphael MichelandGitHub 749f5c7e6c Invoices: Visibly mark paid invoices (#3094) 2023-02-07 11:34:38 +01:00
pajowuandGitHub d1e8504481 Use local name in control variation list heading (#3091) 2023-02-07 10:07:19 +01:00
Raphael Michel 108408366e Fix localization issue in scheduled emails 2023-02-07 10:05:26 +01:00
Raphael MichelandGitHub 4543d8093f Add webhooks for changes to items (#3087) 2023-02-06 17:52:42 +01:00
513a90f976 Subevent list: Add meta-data filter (Z#23114466) (#3083)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-02-06 17:51:47 +01:00
Raphael Michel 6f61155deb Bump to stripe==5.1.* 2023-02-06 16:49:50 +01:00
Raphael Michel 714ce28b6a DateFrameWidget: Fix bug with open-ended timeframes 2023-02-06 13:15:45 +01:00
Raphael Michel b3bcad38a8 DateFrameWidget: Improve dealing with required fields 2023-02-06 13:15:22 +01:00
Raphael Michel 90978e5cab Update from PyPDF2 to pypdf 2023-02-06 10:09:30 +01:00
Raphael Michel 84fb481cdb Fix #3090 -- Add note on pgloader version to migration guide 2023-02-06 10:04:58 +01:00
Raphael Michel 854b41c955 Downgrade PyPDF2 to 2.12 again 2023-02-03 15:00:55 +01:00
Raphael Michel 79ee89bde9 Widget: Add data-id attributes to items, variations and categories 2023-02-03 13:42:05 +01:00
Richard SchreiberandGitHub d8d31bab51 PDF-Export: limit pagesize precision in badges nup-placement (#3085) 2023-02-03 11:59:43 +01:00
Raphael Michel d47bebb403 Update openpyxl to 3.1.* 2023-02-03 09:35:21 +01:00
Raphael Michel 32927bfd4f Update pycryptodome to 3.17.* 2023-02-03 09:35:21 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
41c8d646d9 Bump @babel/core from 7.20.7 to 7.20.12 in /src/pretix/static/npm_dir (#3080)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-03 09:35:19 +01:00
juliusstoerrleandRaphael Michel c828873d21 Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5093 of 5093 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2023-02-03 09:31:50 +01:00
b4e372ce04 PDF: Add support for line height (#3066)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-02-02 19:07:10 +01:00
Raphael Michel 7b301b6027 Self-service order change: Don't create invoice too early 2023-02-01 17:23:28 +01:00
Raphael Michel 68430f01a3 OrderPayment.fail(): Return whether fail was successful 2023-02-01 15:45:20 +01:00
Raphael MichelandRaphael Michel 363c62a6ca Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (5093 of 5093 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2023-02-01 14:55:00 +01:00
Raphael MichelandRaphael Michel a7f32b8647 Translations: Update German
Currently translated at 100.0% (5093 of 5093 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2023-02-01 14:55:00 +01:00
Raphael Michel 8786397910 Add words to translation word lists 2023-02-01 14:52:03 +01:00
Raphael Michel d078a42250 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2023-02-01 14:31:33 +01:00
Raphael Michel f1a73cd440 Update django to 3.2.17 2023-02-01 13:22:54 +01:00
8bba1a2ea6 Fix #1251 -- Event list/calendar: Show "event almost sold out" state (#3063)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2023-02-01 13:20:06 +01:00
Raphael Michel aeb5c52bfe Ignore deprecation warning from PyPDF2 2023-02-01 13:18:50 +01:00
Raphael Michel a684aca212 Revert "Fix test failing due to tz"
This reverts commit 1d46a96821.
2023-02-01 13:16:18 +01:00
Raphael Michel 59d46ddded Revert "First steps into pytz deprecation"
This reverts commit e4e7d50659.
2023-02-01 13:15:18 +01:00
Raphael Michel e4e7d50659 First steps into pytz deprecation 2023-02-01 13:12:24 +01:00
Raphael Michel 1d46a96821 Fix test failing due to tz 2023-02-01 12:27:22 +01:00
Raphael Michel 8b81ef6f43 Fix test classes 2023-02-01 12:27:15 +01:00
Raphael Michel cb734510ac Bump PyPDF2 to 3.0.* 2023-02-01 12:16:30 +01:00
Raphael Michel b29f5c69ed Update isort to 5.12.* 2023-02-01 12:16:01 +01:00
Raphael Michel 56ce37225c Update pytest-rerunfailures to 11.* 2023-02-01 12:15:28 +01:00
Raphael MichelandGitHub afae6fdd45 Improve error messages when event or organizer can't be deleted (#3074) 2023-02-01 11:43:07 +01:00
Mauro AmicoandRaphael Michel 4cad8eae93 Translations: Update Italian
Currently translated at 18.2% (928 of 5085 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/it/

powered by weblate
2023-02-01 11:43:00 +01:00
Raphael Michel c5b7ff66b7 Fix incorrecly used error messages 2023-01-31 22:11:53 +01:00
Raphael Michel 04e16bbb39 Prevent generation of very long team names 2023-01-31 18:13:51 +01:00
Raphael Michel eea48af60a PDF: Fix Arabic string rendering 2023-01-31 17:11:47 +01:00
Raphael Michel fc63f60960 Fix #3064 -- Banktransfer: Incorrect timeout handling 2023-01-31 10:44:47 +01:00
Raphael Michel 3b94125471 Voucher product autocompletion: Allow to search for internal name 2023-01-30 18:20:28 +01:00
Raphael MichelandGitHub 00d901b04b Test on Python 3.11, drop Python 3.7-3.8 (#2982) 2023-01-30 17:54:19 +01:00
Richard SchreiberandGitHub a7f9e100d2 Clean up localization or error messages in cart (#3049) 2023-01-30 17:24:09 +01:00
Raphael Michel 59f409b1c6 Widget: Add missing "var" statement 2023-01-30 17:07:27 +01:00
Raphael Michel e03bebf5ab Bump to 4.17.0.dev0 2023-01-30 13:51:58 +01:00
168 changed files with 32236 additions and 27605 deletions
+2 -2
View File
@@ -26,10 +26,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
- name: Set up Python 3.11
uses: actions/setup-python@v1
with:
python-version: 3.9
python-version: 3.11
- uses: actions/cache@v1
with:
path: ~/.cache/pip
+4 -4
View File
@@ -24,10 +24,10 @@ jobs:
name: Check gettext syntax
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
- name: Set up Python 3.11
uses: actions/setup-python@v1
with:
python-version: 3.9
python-version: 3.11
- uses: actions/cache@v1
with:
path: ~/.cache/pip
@@ -50,10 +50,10 @@ jobs:
name: Spellcheck
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
- name: Set up Python 3.11
uses: actions/setup-python@v1
with:
python-version: 3.9
python-version: 3.11
- uses: actions/cache@v1
with:
path: ~/.cache/pip
+6 -6
View File
@@ -24,10 +24,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
- name: Set up Python 3.11
uses: actions/setup-python@v1
with:
python-version: 3.9
python-version: 3.11
- uses: actions/cache@v1
with:
path: ~/.cache/pip
@@ -45,10 +45,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
- name: Set up Python 3.11
uses: actions/setup-python@v1
with:
python-version: 3.9
python-version: 3.11
- uses: actions/cache@v1
with:
path: ~/.cache/pip
@@ -66,10 +66,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
- name: Set up Python 3.11
uses: actions/setup-python@v1
with:
python-version: 3.9
python-version: 3.11
- name: Install Dependencies
run: pip3 install licenseheaders
- name: Run licenseheaders
+6 -6
View File
@@ -24,22 +24,22 @@ jobs:
name: Tests
strategy:
matrix:
python-version: ["3.7", "3.9", "3.10"]
python-version: ["3.9", "3.10", "3.11"]
database: [sqlite, postgres, mysql]
exclude:
- database: mysql
python-version: "3.10"
- database: mysql
python-version: "3.9"
- database: mysql
python-version: "3.11"
- database: sqlite
python-version: "3.7"
python-version: "3.9"
- database: sqlite
python-version: "3.10"
steps:
- uses: actions/checkout@v2
- uses: getong/mariadb-action@v1.1
with:
mariadb version: '10.4'
mariadb version: '10.10'
mysql database: 'pretix'
mysql root password: ''
if: matrix.database == 'mysql'
@@ -83,4 +83,4 @@ jobs:
file: src/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
if: matrix.database == 'postgres' && matrix.python-version == '3.10'
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.9-bullseye
FROM python:3.11-bullseye
RUN apt-get update && \
apt-get install -y --no-install-recommends \
+2 -2
View File
@@ -23,7 +23,7 @@ installation guides):
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
* A HTTP reverse proxy, e.g. `nginx`_ or Apache to allow HTTPS connections
* A `PostgreSQL`_ 9.6+ database server
* A `PostgreSQL`_ 11+ database server
* A `redis`_ server
* A `nodejs`_ installation
@@ -127,7 +127,7 @@ We now install pretix, its direct dependencies and gunicorn::
(venv)$ pip3 install pretix gunicorn
Note that you need Python 3.7 or newer. You can find out your Python version using ``python -V``.
Note that you need Python 3.9 or newer. You can find out your Python version using ``python -V``.
We also need to create a data directory::
+8
View File
@@ -104,6 +104,12 @@ Install ``pgloader``::
# apt install pgloader
.. note::
If you are using Ubuntu 20.04, the ``pgloader`` version from the repositories seems to be incompatible with PostgreSQL
12+. You can install ``pgloader`` from the `PostgreSQL repositories`_ instead.
See also `this discussion <https://github.com/pretix/pretix/issues/3090>`_.
Create a new file ``/tmp/pretix.load``, replacing the MySQL and PostgreSQL connection strings with the correct user names, passwords, and/or database names::
LOAD DATABASE
@@ -146,3 +152,5 @@ Now, restart pretix. Maybe stop your MySQL server as a verification step that yo
And you're done! After you've verified everything has been copied correctly, you can delete the old MySQL database.
.. note:: Don't forget to update your backup process to back up your PostgreSQL database instead of your MySQL database now.
.. _PostgreSQL repositories: https://wiki.postgresql.org/wiki/Apt
+3
View File
@@ -42,6 +42,7 @@ introductory_text string Text to be prin
additional_text string Text to be printed below the product list
payment_provider_text string Text to be printed below the product list with
payment information
payment_provider_stamp string Short text to be visibly printed to indicate payment status
footer_text string Text to be printed in the page footer area
lines list of objects The actual invoice contents
├ position integer Number of the line within an invoice.
@@ -178,6 +179,7 @@ Endpoints
"internal_reference": "",
"additional_text": "We are looking forward to see you on our conference!",
"payment_provider_text": "Please transfer the money to our account ABC…",
"payment_provider_stamp": null,
"footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321",
"lines": [
{
@@ -268,6 +270,7 @@ Endpoints
"internal_reference": "",
"additional_text": "We are looking forward to see you on our conference!",
"payment_provider_text": "Please transfer the money to our account ABC…",
"payment_provider_stamp": null,
"footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321",
"lines": [
{
+12 -5
View File
@@ -26,6 +26,7 @@ limit_events list of strings If ``all_events
action_types list of strings A list of action type filters that limit the
notifications sent to this webhook. See below for
valid values
comment string Internal comment on this webhook, default ``null``
===================================== ========================== =======================================================
The following values for ``action_types`` are valid with pretix core:
@@ -56,6 +57,7 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.subevent.added``
* ``pretix.subevent.changed``
* ``pretix.subevent.deleted``
* ``pretix.event.item.*``
* ``pretix.event.live.activated``
* ``pretix.event.live.deactivated``
* ``pretix.event.testmode.activated``
@@ -98,7 +100,8 @@ Endpoints
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"],
"comment": null
}
]
}
@@ -135,7 +138,8 @@ Endpoints
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"],
"comment": null
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -162,7 +166,8 @@ Endpoints
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"],
"comment": "Called for changes"
}
**Example response**:
@@ -179,7 +184,8 @@ Endpoints
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"],
"comment": "Called for changes"
}
:param organizer: The ``slug`` field of the organizer to create a webhook for
@@ -224,7 +230,8 @@ Endpoints
"target_url": "https://httpstat.us/200",
"all_events": false,
"limit_events": ["democon"],
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"]
"action_types": ["pretix.event.order.modified", "pretix.event.order.changed.*"],
"comment": null
}
:param organizer: The ``slug`` field of the organizer to modify
+2
View File
@@ -102,6 +102,8 @@ The provider class
.. automethod:: render_invoice_text
.. automethod:: render_invoice_stamp
.. automethod:: order_change_allowed
.. automethod:: payment_prepare
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "4.16.0"
__version__ = "4.17.0.dev0"
@@ -0,0 +1,18 @@
# Generated by Django 3.2.17 on 2023-02-07 12:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixapi', '0009_auto_20221217_1847'),
]
operations = [
migrations.AddField(
model_name='webhook',
name='comment',
field=models.CharField(max_length=255, null=True),
),
]
+1
View File
@@ -112,6 +112,7 @@ class WebHook(models.Model):
target_url = models.URLField(verbose_name=_("Target URL"), max_length=255)
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
comment = models.CharField(verbose_name=_("Comment"), max_length=255, null=True, blank=True)
class Meta:
ordering = ('id',)
+3 -3
View File
@@ -1484,9 +1484,9 @@ class InvoiceSerializer(I18nAwareModelSerializer):
'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode',
'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary',
'custom_field', 'date', 'refers', 'locale',
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date',
'internal_reference')
'introductory_text', 'additional_text', 'payment_provider_text', 'payment_provider_stamp',
'footer_text', 'lines', 'foreign_currency_display', 'foreign_currency_rate',
'foreign_currency_rate_date', 'internal_reference')
class OrderPaymentCreateSerializer(I18nAwareModelSerializer):
+1 -1
View File
@@ -55,7 +55,7 @@ class WebHookSerializer(I18nAwareModelSerializer):
class Meta:
model = WebHook
fields = ('id', 'enabled', 'target_url', 'all_events', 'limit_events', 'action_types')
fields = ('id', 'enabled', 'target_url', 'all_events', 'limit_events', 'action_types', 'comment')
def validate(self, data):
data = super().validate(data)
+23 -27
View File
@@ -96,7 +96,7 @@ def get_all_webhook_events():
return types
class ParametrizedOrderWebhookEvent(WebhookEvent):
class ParametrizedWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
@@ -110,6 +110,8 @@ class ParametrizedOrderWebhookEvent(WebhookEvent):
def verbose_name(self):
return self._verbose_name
class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
order = logentry.content_object
if not order:
@@ -124,19 +126,7 @@ class ParametrizedOrderWebhookEvent(WebhookEvent):
}
class ParametrizedEventWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
super().__init__()
@property
def action_type(self):
return self._action_type
@property
def verbose_name(self):
return self._verbose_name
class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
if logentry.action_type == 'pretix.event.deleted':
@@ -160,19 +150,7 @@ class ParametrizedEventWebhookEvent(WebhookEvent):
}
class ParametrizedSubEventWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
super().__init__()
@property
def action_type(self):
return self._action_type
@property
def verbose_name(self):
return self._verbose_name
class ParametrizedSubEventWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
# do not use content_object, this is also called in deletion
@@ -185,6 +163,19 @@ class ParametrizedSubEventWebhookEvent(WebhookEvent):
}
class ParametrizedItemWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
# do not use content_object, this is also called in deletion
return {
'notification_id': logentry.pk,
'organizer': logentry.event.organizer.slug,
'event': logentry.event.slug,
'item': logentry.object_id,
'action': logentry.action_type,
}
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
def build_payload(self, logentry: LogEntry):
@@ -305,6 +296,11 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.subevent.deleted',
pgettext_lazy('subevent', 'Event series date deleted'),
),
ParametrizedItemWebhookEvent(
'pretix.event.item.*',
_('Product changed (including product added or deleted and including changes to nested objects like '
'variations or bundles)'),
),
ParametrizedEventWebhookEvent(
'pretix.event.live.activated',
_('Shop taken live'),
+42 -8
View File
@@ -35,8 +35,8 @@ from django.utils.formats import date_format, localize
from django.utils.translation import (
get_language, gettext, gettext_lazy, pgettext,
)
from reportlab.lib import pagesizes
from reportlab.lib.enums import TA_LEFT, TA_RIGHT
from reportlab.lib import colors, pagesizes
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
@@ -44,8 +44,8 @@ from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import (
BaseDocTemplate, Frame, KeepTogether, NextPageTemplate, PageTemplate,
Paragraph, Spacer, Table, TableStyle,
BaseDocTemplate, Flowable, Frame, KeepTogether, NextPageTemplate,
PageTemplate, Paragraph, Spacer, Table, TableStyle,
)
from pretix.base.decimal import round_decimal
@@ -147,6 +147,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
"""
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='BoldInverseCenter', fontName=self.font_bold, fontSize=10, leading=12,
textColor=colors.white, alignment=TA_CENTER))
stylesheet.add(ParagraphStyle(name='InvoiceFrom', parent=stylesheet['Normal']))
stylesheet.add(ParagraphStyle(name='Heading1', fontName=self.font_bold, fontSize=15, leading=15 * 1.2))
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName=self.font_bold, fontSize=8, leading=12))
@@ -249,6 +251,31 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
class PaidMarker(Flowable):
def __init__(self, text='paid', color=None, font='OpenSansBd', size=20):
super().__init__()
self.text = text
self.color = color
self.font = font
self.size = size
self._showBoundary = True
def wrap(self, availwidth, availheight):
# Fake a size, we don't care if we exceed the table
return 10, self.size / 2
def draw(self):
self.canv.translate(0, - self.size / 2)
self.canv.rotate(2)
self.canv.setFont(self.font, self.size)
self.canv.setFillColor(self.color)
width = self.canv.stringWidth(self.text, self.font, self.size)
self.canv.drawRightString(0, 0, self.text)
self.canv.setStrokeColor(self.color)
self.canv.roundRect(-width - self.size / 2, -self.size / 4, width + self.size, self.size + self.size / 4, 3)
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
identifier = 'classic'
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
@@ -612,10 +639,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
tdata.append([
pgettext('invoice', 'Invoice total'), '', money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.65, .05, .30)]
colwidths = [a * doc.width for a in (.65, .20, .15)]
if self.invoice.event.settings.invoice_show_payments and not self.invoice.is_cancellation:
if self.invoice.order.status == Order.STATUS_PENDING:
if not self.invoice.is_cancellation:
if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING:
pending_sum = self.invoice.order.pending_sum
if pending_sum != total:
tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [
@@ -627,7 +654,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
]
elif self.invoice.order.payments.filter(
elif self.invoice.event.settings.invoice_show_payments and self.invoice.order.payments.filter(
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED), provider='giftcard'
).exists():
giftcard_sum = self.invoice.order.payments.filter(
@@ -645,6 +672,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
]
elif self.invoice.payment_provider_stamp:
pm = PaidMarker(
text=self.invoice.payment_provider_stamp,
color=colors.HexColor(self.event.settings.theme_color_success),
size=16
)
tdata[-1][-2] = pm
table = Table(tdata, colWidths=colwidths, repeatRows=1)
table.setStyle(TableStyle(tstyledata))
@@ -0,0 +1,19 @@
# Generated by Django 3.2.17 on 2023-02-07 10:00
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0228_scheduledeventexport_scheduledorganizerexport'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='payment_provider_stamp',
field=models.CharField(max_length=100, null=True),
),
]
+82 -41
View File
@@ -36,7 +36,7 @@ import logging
import os
import string
import uuid
from collections import OrderedDict
from collections import Counter, OrderedDict, defaultdict
from datetime import datetime, time, timedelta
from operator import attrgetter
from urllib.parse import urljoin
@@ -340,64 +340,104 @@ class EventMixin:
)
)
@cached_property
@property
def best_availability_state(self):
return self.best_availability[0]
@property
def best_availability_is_low(self):
"""
Returns ``True`` if the availability of tickets in this event is lower than the percentage
given in setting ``low_availability_percentage``.
"""
if not self.settings.low_availability_percentage:
return False
ba = self.best_availability
if ba[1] is None or not ba[2]:
return False
percentage = ba[1] / ba[2] * 100
return percentage < self.settings.low_availability_percentage
@cached_property
def best_availability(self):
"""
Returns a 3-tuple of
- The availability state of this event (one of the ``Quota.AVAILABILITY_*`` constants)
- The number of tickets currently available (or ``None``)
- The number of tickets "originally" available (or ``None``)
This can only be called on objects obtained through a queryset that has been passed through ``.annotated()``.
"""
from .items import Quota
if not hasattr(self, 'active_quotas'):
raise TypeError("Call this only if you fetched the subevents via Event/SubEvent.annotated()")
items_available = set()
vars_available = set()
items_reserved = set()
vars_reserved = set()
items_gone = set()
vars_gone = set()
items_disabled = set()
vars_disabled = set()
if hasattr(self, 'disabled_items'): # SubEventItem
items_disabled = set(self.disabled_items.split(","))
else:
items_disabled = set()
if hasattr(self, 'disabled_vars'): # SubEventItemVariation
vars_disabled = set(self.disabled_vars.split(","))
else:
vars_disabled = set()
# Compute the availability of all quotas and build a item→quotas mapping with all non-disabled items
r = getattr(self, '_quota_cache', {})
quotas_for_item = defaultdict(list)
quotas_for_variation = defaultdict(list)
for q in self.active_quotas:
res = r[q] if q in r else q.availability(allow_cache=True)
if q not in r:
r[q] = q.availability(allow_cache=True)
if res[0] == Quota.AVAILABILITY_OK:
if q.active_items:
items_available.update(q.active_items.split(","))
if q.active_variations:
vars_available.update(q.active_variations.split(","))
elif res[0] == Quota.AVAILABILITY_RESERVED:
if q.active_items:
items_reserved.update(q.active_items.split(","))
if q.active_variations:
vars_reserved.update(q.active_variations.split(","))
elif res[0] < Quota.AVAILABILITY_RESERVED:
if q.active_items:
items_gone.update(q.active_items.split(","))
if q.active_variations:
vars_gone.update(q.active_variations.split(","))
if q.active_items:
for item_id in q.active_items.split(","):
if item_id not in items_disabled:
quotas_for_item[item_id].append(q)
if q.active_variations:
for var_id in q.active_variations.split(","):
if var_id not in vars_disabled:
quotas_for_variation[var_id].append(q)
items_available -= items_disabled
items_reserved -= items_disabled
items_gone -= items_disabled
vars_available -= vars_disabled
vars_reserved -= vars_disabled
vars_gone -= vars_disabled
if not self.active_quotas or (not quotas_for_item and not quotas_for_variation):
# No item is enabled for this event, treat the event as "unknown"
return None, None, None
if not self.active_quotas or (
not items_available and not items_reserved and not items_gone and not vars_gone and not vars_available and not vars_reserved
):
return None
# We iterate over all items and variations and keep track of
# - `best_state_found` - the best availability state we have seen so far. If one item is available, the event is available!
# - `num_tickets_found` - the number of tickets currently available in total. We sum up all the items and variations, but keep
# track of them per-quota in `quota_used_for_found_tickets` to make sure we don't count the same tickets twice if two or more
# items share the same quota
# - `num_tickets_possible` - basically the same thing, just with the total size of quotas instead of their currently availability
# since we need that for the percentage calculation
best_state_found = Quota.AVAILABILITY_GONE
num_tickets_found = 0
num_tickets_possible = 0
quota_used_for_found_tickets = Counter()
quota_used_for_possible_tickets = Counter()
for quota_list in list(quotas_for_item.values()) + list(quotas_for_variation.values()):
worst_state_for_ticket = min(r[q][0] for q in quota_list)
quotas_that_are_not_unlimited = [q for q in quota_list if q.size is not None]
if not quotas_that_are_not_unlimited:
# We found an unlimited ticket, no more need to do anything else
return Quota.AVAILABILITY_OK, None, None
if items_available - items_reserved - items_gone or vars_available - vars_reserved - vars_gone:
return Quota.AVAILABILITY_OK
if items_reserved - items_gone or vars_reserved - vars_gone:
return Quota.AVAILABILITY_RESERVED
return Quota.AVAILABILITY_GONE
if worst_state_for_ticket == Quota.AVAILABILITY_OK:
availability_of_this = min(max(0, r[q][1] - quota_used_for_found_tickets[q]) for q in quotas_that_are_not_unlimited)
num_tickets_found += availability_of_this
for q in quota_list:
quota_used_for_found_tickets[q] += availability_of_this
possible_of_this = min(max(0, q.size - quota_used_for_possible_tickets[q]) for q in quotas_that_are_not_unlimited)
num_tickets_possible += possible_of_this
for q in quota_list:
quota_used_for_possible_tickets[q] += possible_of_this
best_state_found = max(best_state_found, worst_state_for_ticket)
return best_state_found, num_tickets_found, num_tickets_possible
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
qs_annotated = self._seats(ignore_voucher=ignore_voucher)
@@ -591,6 +631,7 @@ class Event(EventMixin, LoggedModel):
self.settings.invoice_email_attachment = True
self.settings.name_scheme = 'given_family'
self.settings.payment_banktransfer_invoice_immediately = True
self.settings.low_availability_percentage = 10
@property
def social_image(self):
+3
View File
@@ -95,6 +95,8 @@ class Invoice(models.Model):
:type additional_text: str
:param payment_provider_text: A payment provider specific text
:type payment_provider_text: str
:param payment_provider_stamp: A payment provider specific stamp
:type payment_provider_stamp: str
:param footer_text: A footer text, displayed smaller and centered on every page
:type footer_text: str
:param foreign_currency_display: A different currency that taxes should also be displayed in.
@@ -144,6 +146,7 @@ class Invoice(models.Model):
additional_text = models.TextField(blank=True)
reverse_charge = models.BooleanField(default=False)
payment_provider_text = models.TextField(blank=True)
payment_provider_stamp = models.CharField(max_length=100, null=True, blank=True)
footer_text = models.TextField(blank=True)
foreign_currency_display = models.CharField(max_length=50, null=True, blank=True)
+2 -1
View File
@@ -1635,7 +1635,7 @@ class OrderPayment(models.Model):
logger.info('Failed payment {} but ignored due to likely race condition.'.format(
self.full_id,
))
return
return False
if isinstance(info, str):
locked_instance.info = info
@@ -1651,6 +1651,7 @@ class OrderPayment(models.Model):
'info': info,
'data': log_data,
}, user=user, auth=auth)
return True
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
ignore_date=False, lock=True, payment_date=None, generate_invoice=True):
+10
View File
@@ -464,6 +464,16 @@ class BasePaymentProvider:
return pgettext_lazy('invoice', 'The payment for this invoice has already been received.')
return self.settings.get('_invoice_text', as_type=LazyI18nString, default='')
def render_invoice_stamp(self, order: Order, payment: OrderPayment) -> str:
"""
This is called when an invoice for an order with this payment provider is generated.
The default implementation returns "paid" if the order was already paid, and ``None``
otherwise. You can override this with a string, but it should be *really* short to make
the invoice look pretty.
"""
if order.status == Order.STATUS_PAID:
return _('paid')
@property
def payment_form_fields(self) -> dict:
"""
+22 -9
View File
@@ -62,7 +62,7 @@ from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.strings import LazyI18nString
from PyPDF2 import PdfReader
from pypdf import PdfReader
from pytz import timezone
from reportlab.graphics import renderPDF
from reportlab.graphics.barcode.qr import QrCodeWidget
@@ -848,12 +848,17 @@ class Renderer:
'center': TA_CENTER,
'right': TA_RIGHT
}
# lineheight display differs from browser canvas. This calc is just empirical values to get
# reportlab render similarly to browser canvas.
# for backwards compatability use „uncorrected“ lineheight of 1.0 instead of 1.15
lineheight = float(o['lineheight']) * 1.15 if 'lineheight' in o else 1.0
style = ParagraphStyle(
name=uuid.uuid4().hex,
fontName=font,
fontSize=float(o['fontsize']),
leading=float(o['fontsize']),
autoLeading="max",
leading=lineheight * float(o['fontsize']),
# for backwards compatability use autoLeading if no lineheight is given
autoLeading='off' if 'lineheight' in o else 'max',
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
alignment=align_map[o['align']]
)
@@ -862,6 +867,10 @@ class Renderer:
self._get_text_content(op, order, o) or "",
).replace("\n", "<br/>\n").replace("-", "-&hairsp;")
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
@@ -869,9 +878,6 @@ class Renderer:
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
# reportlab does not support unicode combination characters
text = unicodedata.normalize("NFKC", text)
p = Paragraph(text, style=style)
w, h = p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
@@ -881,8 +887,15 @@ class Renderer:
if o.get('downward', False):
canvas.translate(float(o['left']) * mm, float(o['bottom']) * mm)
canvas.rotate(o.get('rotation', 0) * -1)
p.drawOn(canvas, 0, -h - ad[1] / 2)
p.drawOn(canvas, 0, -h - ad[1] / 2.5)
else:
if lineheight != 1.0:
# lineheight adds to ascent/descent offsets, just empirical values again to get
# reportlab to render similarly to browser canvas
ad = (
ad[0],
ad[1] + (lineheight - 1.0) * float(o['fontsize']) * 1.05
)
canvas.translate(float(o['left']) * mm, float(o['bottom']) * mm + h)
canvas.rotate(o.get('rotation', 0) * -1)
p.drawOn(canvas, 0, -h - ad[1])
@@ -937,8 +950,8 @@ class Renderer:
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
return BytesIO(f.read())
else:
from PyPDF2 import PdfReader, PdfWriter, Transformation
from PyPDF2.generic import RectangleObject
from pypdf import PdfReader, PdfWriter, Transformation
from pypdf.generic import RectangleObject
buffer.seek(0)
new_pdf = PdfReader(buffer)
output = PdfWriter()
+121 -82
View File
@@ -43,7 +43,9 @@ from django.db import DatabaseError, transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value
from django.dispatch import receiver
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext as _, pgettext_lazy
from django.utils.translation import (
gettext as _, gettext_lazy, ngettext_lazy, pgettext_lazy,
)
from django_scopes import scopes_disabled
from pretix.base.channels import get_all_sales_channels
@@ -81,74 +83,119 @@ class CartError(Exception):
if msgargs:
msg = _(msg) % msgargs
else:
msg = _(msg)
# force msg to string to make sure lazy-translation is done in current locale-context
# otherwise translation might happen in celery-context, which uses default-locale
# also translate with _/gettext to keep it backwards compatible
msg = _(str(msg))
super().__init__(msg)
error_messages = {
'busy': _('We were not able to process your request completely as the '
'server was too busy. Please try again.'),
'empty': _('You did not select any products.'),
'unknown_position': _('Unknown cart position.'),
'busy': gettext_lazy(
'We were not able to process your request completely as the '
'server was too busy. Please try again.'
),
'empty': gettext_lazy('You did not select any products.'),
'unknown_position': gettext_lazy('Unknown cart position.'),
'subevent_required': pgettext_lazy('subevent', 'No date was specified.'),
'not_for_sale': _('You selected a product which is not available for sale.'),
'unavailable': _('Some of the products you selected are no longer available. '
'Please see below for details.'),
'in_part': _('Some of the products you selected are no longer available in '
'the quantity you selected. Please see below for details.'),
'max_items': _("You cannot select more than %s items per order."),
'max_items_per_product': _("You cannot select more than %(max)s items of the product %(product)s."),
'min_items_per_product': _("You need to select at least %(min)s items of the product %(product)s."),
'min_items_per_product_removed': _("We removed %(product)s from your cart as you can not buy less than "
"%(min)s items of it."),
'not_started': _('The booking period for this event has not yet started.'),
'ended': _('The booking period for this event has ended.'),
'payment_ended': _('All payments for this event need to be confirmed already, so no new orders can be created.'),
'some_subevent_not_started': _('The booking period for this event has not yet started. The affected positions '
'have been removed from your cart.'),
'some_subevent_ended': _('The booking period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'),
'price_too_high': _('The entered price is to high.'),
'voucher_invalid': _('This voucher code is not known in our database.'),
'voucher_min_usages': _('The voucher code "%(voucher)s" can only be used if you select at least %(number)s '
'matching products.'),
'voucher_min_usages_removed': _('The voucher code "%(voucher)s" can only be used if you select at least '
'%(number)s matching products. We have therefore removed some positions from '
'your cart that can no longer be purchased like this.'),
'voucher_redeemed': _('This voucher code has already been used the maximum number of times allowed.'),
'voucher_redeemed_cart': _('This voucher code is currently locked since it is already contained in a cart. This '
'might mean that someone else is redeeming this voucher right now, or that you tried '
'to redeem it before but did not complete the checkout process. You can try to use it '
'again in %d minutes.'),
'voucher_redeemed_partial': _('This voucher code can only be redeemed %d more times.'),
'voucher_double': _('You already used this voucher code. Remove the associated line from your '
'cart if you want to use it for a different product.'),
'voucher_expired': _('This voucher is expired.'),
'voucher_invalid_item': _('This voucher is not valid for this product.'),
'voucher_invalid_seat': _('This voucher is not valid for this seat.'),
'voucher_no_match': _('We did not find any position in your cart that we could use this voucher for. If you want '
'to add something new to your cart using that voucher, you can do so with the voucher '
'redemption option on the bottom of the page.'),
'voucher_item_not_available': _(
'not_for_sale': gettext_lazy('You selected a product which is not available for sale.'),
'unavailable': gettext_lazy(
'Some of the products you selected are no longer available. '
'Please see below for details.'
),
'in_part': gettext_lazy(
'Some of the products you selected are no longer available in '
'the quantity you selected. Please see below for details.'
),
'max_items': ngettext_lazy(
"You cannot select more than %s item per order.",
"You cannot select more than %s items per order."
),
'max_items_per_product': ngettext_lazy(
"You cannot select more than %(max)s item of the product %(product)s.",
"You cannot select more than %(max)s items of the product %(product)s.",
"max"
),
'min_items_per_product': ngettext_lazy(
"You need to select at least %(min)s item of the product %(product)s.",
"You need to select at least %(min)s items of the product %(product)s.",
"min"
),
'min_items_per_product_removed': ngettext_lazy(
"We removed %(product)s from your cart as you can not buy less than %(min)s item of it.",
"We removed %(product)s from your cart as you can not buy less than %(min)s items of it.",
"min"
),
'not_started': gettext_lazy('The booking period for this event has not yet started.'),
'ended': gettext_lazy('The booking period for this event has ended.'),
'payment_ended': gettext_lazy('All payments for this event need to be confirmed already, so no new orders can be created.'),
'some_subevent_not_started': gettext_lazy(
'The booking period for this event has not yet started. The affected positions '
'have been removed from your cart.'),
'some_subevent_ended': gettext_lazy(
'The booking period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'),
'price_too_high': gettext_lazy('The entered price is to high.'),
'voucher_invalid': gettext_lazy('This voucher code is not known in our database.'),
'voucher_min_usages': gettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s '
'matching products.'
),
'voucher_min_usages_removed': ngettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products. '
'We have therefore removed some positions from your cart that can no longer be purchased like this.',
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products. '
'We have therefore removed some positions from your cart that can no longer be purchased like this.',
'number'
),
'voucher_redeemed': gettext_lazy('This voucher code has already been used the maximum number of times allowed.'),
'voucher_redeemed_cart': gettext_lazy(
'This voucher code is currently locked since it is already contained in a cart. This '
'might mean that someone else is redeeming this voucher right now, or that you tried '
'to redeem it before but did not complete the checkout process. You can try to use it '
'again in %d minutes.'
),
'voucher_redeemed_partial': gettext_lazy('This voucher code can only be redeemed %d more times.'),
'voucher_whole_cart_not_combined': gettext_lazy('Applying a voucher to the whole cart should not be combined with other operations.'),
'voucher_double': gettext_lazy(
'You already used this voucher code. Remove the associated line from your '
'cart if you want to use it for a different product.'
),
'voucher_expired': gettext_lazy('This voucher is expired.'),
'voucher_invalid_item': gettext_lazy('This voucher is not valid for this product.'),
'voucher_invalid_seat': gettext_lazy('This voucher is not valid for this seat.'),
'voucher_no_match': gettext_lazy(
'We did not find any position in your cart that we could use this voucher for. If you want '
'to add something new to your cart using that voucher, you can do so with the voucher '
'redemption option on the bottom of the page.'
),
'voucher_item_not_available': gettext_lazy(
'Your voucher is valid for a product that is currently not for sale.'),
'voucher_invalid_subevent': pgettext_lazy('subevent', 'This voucher is not valid for this event date.'),
'voucher_required': _('You need a valid voucher code to order this product.'),
'voucher_required': gettext_lazy('You need a valid voucher code to order this product.'),
'inactive_subevent': pgettext_lazy('subevent', 'The selected event date is not active.'),
'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.'),
'addon_only': _('One of the products you selected can only be bought as an add-on to another product.'),
'bundled_only': _('One of the products you selected can only be bought part of a bundle.'),
'seat_required': _('You need to select a specific seat.'),
'seat_invalid': _('Please select a valid seat.'),
'seat_forbidden': _('You can not select a seat for this position.'),
'seat_unavailable': _('The seat you selected has already been taken. Please select a different seat.'),
'seat_multiple': _('You can not select the same seat multiple times.'),
'gift_card': _("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."),
'country_blocked': _('One of the selected products is not available in the selected country.'),
'addon_invalid_base': gettext_lazy('You can not select an add-on for the selected product.'),
'addon_duplicate_item': gettext_lazy('You can not select two variations of the same add-on product.'),
'addon_max_count': ngettext_lazy(
'You can select at most %(max)s add-on from the category %(cat)s for the product %(base)s.',
'You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.',
'max'
),
'addon_min_count': ngettext_lazy(
'You need to select at least %(min)s add-on from the category %(cat)s for the product %(base)s.',
'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.',
'min'
),
'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
'addon_only': gettext_lazy('One of the products you selected can only be bought as an add-on to another product.'),
'bundled_only': gettext_lazy('One of the products you selected can only be bought part of a bundle.'),
'seat_required': gettext_lazy('You need to select a specific seat.'),
'seat_invalid': gettext_lazy('Please select a valid seat.'),
'seat_forbidden': gettext_lazy('You can not select a seat for this position.'),
'seat_unavailable': gettext_lazy('The seat you selected has already been taken. Please select a different seat.'),
'seat_multiple': gettext_lazy('You can not select the same seat multiple times.'),
'gift_card': gettext_lazy("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."),
'country_blocked': gettext_lazy('One of the selected products is not available in the selected country.'),
}
@@ -320,8 +367,7 @@ class CartManager:
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
not op.position.addon_to_id])
if cartsize > int(self.event.settings.max_items_per_order):
# TODO: i18n plurals
raise CartError(_(error_messages['max_items']) % (self.event.settings.max_items_per_order,))
raise CartError(error_messages['max_items'] % self.event.settings.max_items_per_order)
def _check_item_constraints(self, op, current_ops=[]):
if isinstance(op, (self.AddOperation, self.ExtendOperation)):
@@ -494,7 +540,7 @@ class CartManager:
def apply_voucher(self, voucher_code: str):
if self._operations:
raise CartError('Applying a voucher to the whole cart should not be combined with other operations.')
raise CartError(error_messages['voucher_whole_cart_not_combined'])
try:
voucher = self.event.vouchers.get(code__iexact=voucher_code.strip())
except Voucher.DoesNotExist:
@@ -539,7 +585,7 @@ class CartManager:
for voucher, cnt in list(voucher_use_diff.items()):
if 0 < cnt < voucher.min_usages_remaining:
raise CartError(
_(error_messages['voucher_min_usages']) % {
error_messages['voucher_min_usages'] % {
'voucher': voucher.code,
'number': voucher.min_usages_remaining,
}
@@ -843,22 +889,16 @@ class CartManager:
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 CartError(
error_messages['addon_max_count'],
{
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 CartError(
error_messages['addon_min_count'],
{
error_messages['addon_min_count'] % {
'base': str(item.name),
'min': iao.min_count,
'cat': str(iao.addon_category.name),
@@ -866,8 +906,7 @@ class CartManager:
)
elif any(v > 1 for v in n_per_i.values()) and not iao.multi_allowed:
raise CartError(
error_messages['addon_no_multi'],
{
error_messages['addon_no_multi'] % {
'base': str(item.name),
'cat': str(iao.addon_category.name),
}
@@ -931,7 +970,7 @@ class CartManager:
if item.max_per_order and count > item.max_per_order:
raise CartError(
_(error_messages['max_items_per_product']) % {
error_messages['max_items_per_product'] % {
'max': item.max_per_order,
'product': item.name
}
@@ -945,13 +984,13 @@ class CartManager:
for p in self.positions:
if p.item_id == item.pk and p.pk not in removals:
self._operations.append(self.RemoveOperation(position=p))
err = _(error_messages['min_items_per_product_removed']) % {
err = error_messages['min_items_per_product_removed'] % {
'min': item.min_per_order,
'product': item.name
}
if not err:
raise CartError(
_(error_messages['min_items_per_product']) % {
error_messages['min_items_per_product'] % {
'min': item.min_per_order,
'product': item.name
}
@@ -980,13 +1019,13 @@ class CartManager:
for p in self.positions:
if p.voucher_id == voucher.pk and p.pk not in removals:
self._operations.append(self.RemoveOperation(position=p))
err = _(error_messages['voucher_min_usages_removed']) % {
err = error_messages['voucher_min_usages_removed'] % {
'voucher': voucher.code,
'number': voucher.min_usages_remaining,
}
if not err:
raise CartError(
_(error_messages['voucher_min_usages']) % {
error_messages['voucher_min_usages'] % {
'voucher': voucher.code,
'number': voucher.min_usages_remaining,
}
@@ -1034,11 +1073,11 @@ class CartManager:
if voucher_available_count < 1:
if op.voucher in self._voucher_depend_on_cart:
err = err or _(error_messages['voucher_redeemed_cart']) % self.event.settings.reservation_time
err = err or (error_messages['voucher_redeemed_cart'] % self.event.settings.reservation_time)
else:
err = err or error_messages['voucher_redeemed']
elif voucher_available_count < requested_count:
err = err or _(error_messages['voucher_redeemed_partial']) % voucher_available_count
err = err or (error_messages['voucher_redeemed_partial'] % voucher_available_count)
available_count = min(quota_available_count, voucher_available_count)
+5
View File
@@ -98,8 +98,10 @@ def build_invoice(invoice: Invoice) -> Invoice:
payment = str(lp.payment_provider.render_invoice_text(invoice.order, lp))
else:
payment = str(lp.payment_provider.render_invoice_text(invoice.order))
payment_stamp = lp.payment_provider.render_invoice_stamp(invoice.order, lp)
else:
payment = ""
payment_stamp = None
if invoice.event.settings.invoice_include_expire_date and invoice.order.status == Order.STATUS_PENDING:
if payment:
payment += "<br /><br />"
@@ -111,6 +113,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.additional_text = str(additional).replace('\n', '<br />')
invoice.footer_text = str(footer)
invoice.payment_provider_text = str(payment).replace('\n', '<br />')
invoice.payment_provider_stamp = str(payment_stamp) if payment_stamp else None
try:
ia = invoice.order.invoice_address
@@ -325,6 +328,7 @@ def generate_cancellation(invoice: Invoice, trigger_pdf=True):
cancellation.is_cancellation = True
cancellation.date = timezone.now().date()
cancellation.payment_provider_text = ''
cancellation.payment_provider_stamp = ''
cancellation.file = None
cancellation.sent_to_organizer = None
cancellation.sent_to_customer = None
@@ -436,6 +440,7 @@ def build_preview_invoice_pdf(event):
invoice.additional_text = str(additional).replace('\n', '<br />')
invoice.footer_text = str(footer)
invoice.payment_provider_text = str(payment).replace('\n', '<br />')
invoice.payment_provider_stamp = _('paid')
invoice.invoice_to_name = _("John Doe")
invoice.invoice_to_street = _("214th Example Street")
invoice.invoice_to_zipcode = _("012345")
+120 -90
View File
@@ -54,15 +54,13 @@ from django.db.transaction import get_connection
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext as _
from django.utils.translation import gettext as _, gettext_lazy, ngettext_lazy
from django_scopes import scopes_disabled
from pretix.api.models import OAuthApplication
from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_email_context
from pretix.base.i18n import (
LazyLocaleException, get_language_without_region, language,
)
from pretix.base.i18n import get_language_without_region, language
from pretix.base.models import (
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Membership,
Order, OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User,
@@ -101,47 +99,94 @@ from pretix.helpers import OF_SELF
from pretix.helpers.models import modelcopy
from pretix.helpers.periodic import minimum_interval
class OrderError(Exception):
def __init__(self, *args):
msg = args[0]
msgargs = args[1] if len(args) > 1 else None
self.args = args
if msgargs:
msg = _(msg) % msgargs
else:
# force msg to string to make sure lazy-translation is done in current locale-context
# otherwise translation might happen in celery-context, which uses default-locale
# also translate with _/gettext to keep it backwards compatible
msg = _(str(msg))
super().__init__(msg)
error_messages = {
'unavailable': _('Some of the products you selected were no longer available. '
'Please see below for details.'),
'in_part': _('Some of the products you selected were no longer available in '
'the quantity you selected. Please see below for details.'),
'price_changed': _('The price of some of the items in your cart has changed in the '
'meantime. Please see below for details.'),
'internal': _("An internal error occurred, please try again."),
'empty': _("Your cart is empty."),
'max_items_per_product': _("You cannot select more than %(max)s items of the product %(product)s. We removed the "
"surplus items from your cart."),
'busy': _('We were not able to process your request completely as the '
'server was too busy. Please try again.'),
'not_started': _('The booking period for this event has not yet started.'),
'ended': _('The booking period has ended.'),
'voucher_min_usages': _('The voucher code "%(voucher)s" can only be used if you select at least %(number)s '
'matching products.'),
'voucher_invalid': _('The voucher code used for one of the items in your cart is not known in our database.'),
'voucher_redeemed': _('The voucher code used for one of the items in your cart has already been used the maximum '
'number of times allowed. We removed this item from your cart.'),
'voucher_budget_used': _('The voucher code used for one of the items in your cart has already been too often. We '
'adjusted the price of the item in your cart.'),
'voucher_expired': _('The voucher code used for one of the items in your cart is expired. We removed this item '
'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.'),
'some_subevent_not_started': _('The booking 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 booking period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'),
'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.'),
'unavailable': gettext_lazy(
'Some of the products you selected were no longer available. '
'Please see below for details.'
),
'in_part': gettext_lazy(
'Some of the products you selected were no longer available in '
'the quantity you selected. Please see below for details.'
),
'price_changed': gettext_lazy(
'The price of some of the items in your cart has changed in the '
'meantime. Please see below for details.'
),
'internal': gettext_lazy("An internal error occurred, please try again."),
'empty': gettext_lazy("Your cart is empty."),
'max_items_per_product': ngettext_lazy(
"You cannot select more than %(max)s item of the product %(product)s. We removed the surplus items from your cart.",
"You cannot select more than %(max)s items of the product %(product)s. We removed the surplus items from your cart.",
"max"
),
'busy': gettext_lazy(
'We were not able to process your request completely as the '
'server was too busy. Please try again.'
),
'not_started': gettext_lazy('The booking period for this event has not yet started.'),
'ended': gettext_lazy('The booking period has ended.'),
'voucher_min_usages': ngettext_lazy(
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products.',
'The voucher code "%(voucher)s" can only be used if you select at least %(number)s matching products.',
'number'
),
'voucher_invalid': gettext_lazy('The voucher code used for one of the items in your cart is not known in our database.'),
'voucher_redeemed': gettext_lazy(
'The voucher code used for one of the items in your cart has already been used the maximum '
'number of times allowed. We removed this item from your cart.'
),
'voucher_budget_used': gettext_lazy(
'The voucher code used for one of the items in your cart has already been too often. We '
'adjusted the price of the item in your cart.'
),
'voucher_expired': gettext_lazy(
'The voucher code used for one of the items in your cart is expired. We removed this item from your cart.'
),
'voucher_invalid_item': gettext_lazy(
'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': gettext_lazy('You need a valid voucher code to order one of the products.'),
'some_subevent_not_started': gettext_lazy(
'The booking 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': gettext_lazy(
'The booking period for one of the events in your cart has ended. The affected '
'positions have been removed from your cart.'
),
'seat_invalid': gettext_lazy('One of the seats in your order was invalid, we removed the position from your cart.'),
'seat_unavailable': gettext_lazy('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
'country_blocked': gettext_lazy('One of the selected products is not available in the selected country.'),
'not_for_sale': gettext_lazy('You selected a product which is not available for sale.'),
'addon_invalid_base': gettext_lazy('You can not select an add-on for the selected product.'),
'addon_duplicate_item': gettext_lazy('You can not select two variations of the same add-on product.'),
'addon_max_count': ngettext_lazy(
'You can select at most %(max)s add-on from the category %(cat)s for the product %(base)s.',
'You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.',
'max'
),
'addon_min_count': ngettext_lazy(
'You need to select at least %(min)s add-on from the category %(cat)s for the product %(base)s.',
'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.',
'min'
),
'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
}
logger = logging.getLogger(__name__)
@@ -157,7 +202,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
enough quota.
"""
if order.status != Order.STATUS_CANCELED:
raise OrderError('The order was not canceled.')
raise OrderError(_('The order was not canceled.'))
with order.event.lock() as now_dt:
is_available = order._is_still_available(now_dt, count_waitinglist=False, check_voucher_usage=True,
@@ -538,18 +583,6 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
return order.pk
class OrderError(LazyLocaleException):
def __init__(self, *args):
msg = args[0]
msgargs = args[1] if len(args) > 1 else None
self.args = args
if msgargs:
msg = _(msg) % msgargs
else:
msg = _(msg)
super().__init__(msg)
def _check_date(event: Event, now_dt: datetime):
if event.presale_start and now_dt < event.presale_start:
raise OrderError(error_messages['not_started'])
@@ -570,7 +603,6 @@ def _check_date(event: Event, now_dt: datetime):
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None,
sales_channel='web', customer=None):
err = None
errargs = None
_check_date(event, now_dt)
products_seen = Counter()
@@ -607,9 +639,10 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
products_seen[cp.item] += 1
if cp.item.max_per_order and products_seen[cp.item] > cp.item.max_per_order:
err = error_messages['max_items_per_product']
errargs = {'max': cp.item.max_per_order,
'product': cp.item.name}
err = error_messages['max_items_per_product'] % {
'max': cp.item.max_per_order,
'product': cp.item.name
}
delete(cp)
break
@@ -728,7 +761,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
for voucher, cnt in v_usages.items():
if 0 < cnt < voucher.min_usages_remaining:
raise OrderError(error_messages['voucher_min_usages'], {
raise OrderError(error_messages['voucher_min_usages'] % {
'voucher': voucher.code,
'number': voucher.min_usages_remaining,
})
@@ -791,7 +824,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
cp.save()
if err:
raise OrderError(err, errargs)
raise OrderError(err)
def _get_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress,
@@ -1344,22 +1377,26 @@ def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
class OrderChangeManager:
error_messages = {
'product_without_variation': _('You need to select a variation of the product.'),
'quota': _('The quota {name} does not have enough capacity left to perform the operation.'),
'quota_missing': _('There is no quota defined that allows this operation.'),
'product_invalid': _('The selected product is not active or has no price set.'),
'complete_cancel': _('This operation would leave the order empty. Please cancel the order itself instead.'),
'paid_to_free_exceeded': _('This operation would make the order free and therefore immediately paid, however '
'no quota is available.'),
'addon_to_required': _('This is an add-on product, please select the base position it should be added to.'),
'addon_invalid': _('The selected base position does not allow you to add this product as an add-on.'),
'subevent_required': _('You need to choose a subevent for the new position.'),
'seat_unavailable': _('The selected seat "{seat}" is not available.'),
'seat_subevent_mismatch': _('You selected seat "{seat}" for a date that does not match the selected ticket date. Please choose a seat again.'),
'seat_required': _('The selected product requires you to select a seat.'),
'seat_forbidden': _('The selected product does not allow to select a seat.'),
'tax_rule_country_blocked': _('The selected country is blocked by your tax rule.'),
'gift_card_change': _('You cannot change the price of a position that has been used to issue a gift card.'),
'product_without_variation': gettext_lazy('You need to select a variation of the product.'),
'quota': gettext_lazy('The quota {name} does not have enough capacity left to perform the operation.'),
'quota_missing': gettext_lazy('There is no quota defined that allows this operation.'),
'product_invalid': gettext_lazy('The selected product is not active or has no price set.'),
'complete_cancel': gettext_lazy('This operation would leave the order empty. Please cancel the order itself instead.'),
'paid_to_free_exceeded': gettext_lazy(
'This operation would make the order free and therefore immediately paid, however '
'no quota is available.'
),
'addon_to_required': gettext_lazy('This is an add-on product, please select the base position it should be added to.'),
'addon_invalid': gettext_lazy('The selected base position does not allow you to add this product as an add-on.'),
'subevent_required': gettext_lazy('You need to choose a subevent for the new position.'),
'seat_unavailable': gettext_lazy('The selected seat "{seat}" is not available.'),
'seat_subevent_mismatch': gettext_lazy(
'You selected seat "{seat}" for a date that does not match the selected ticket date. Please choose a seat again.'
),
'seat_required': gettext_lazy('The selected product requires you to select a seat.'),
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
}
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
@@ -1766,22 +1803,16 @@ class OrderChangeManager:
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'],
{
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'],
{
error_messages['addon_min_count'] % {
'base': str(item.name),
'min': iao.min_count,
'cat': str(iao.addon_category.name),
@@ -1789,8 +1820,7 @@ class OrderChangeManager:
)
elif any(v > 1 for v in n_per_i.values()) and not iao.multi_allowed:
raise OrderError(
error_messages['addon_no_multi'],
{
error_messages['addon_no_multi'] % {
'base': str(item.name),
'cat': str(iao.addon_category.name),
}
@@ -2471,7 +2501,7 @@ def perform_order(self, event: Event, payments: List[dict], positions: List[str]
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
raise OrderError(str(error_messages['busy']))
raise OrderError(error_messages['busy'])
_unset = object()
+19
View File
@@ -1308,6 +1308,25 @@ DEFAULTS = {
"the email. Does not affect orders performed through other sales channels."),
)
},
'low_availability_percentage': {
'default': None,
'type': int,
'serializer_class': serializers.IntegerField,
'form_class': forms.IntegerField,
'serializer_kwargs': dict(
min_value=0,
max_value=100,
),
'form_kwargs': dict(
label=_('Low availability threshold'),
help_text=_('If the availability of tickets falls below this percentage, the event (or a date, if it is an '
'event series) will be highlighted to have low availability in the event list or calendar. If '
'you keep this option empty, low availability will not be shown publicly.'),
min_value=0,
max_value=100,
required=False
)
},
'event_list_availability': {
'default': 'True',
'type': bool,
+4 -2
View File
@@ -293,14 +293,16 @@ class DateFrameWidget(forms.MultiWidget):
if '/' in value:
return [
'custom',
date.fromisoformat(value.split('/', 1)[0]),
date.fromisoformat(value.split('/', 1)[-1]),
date.fromisoformat(value.split('/', 1)[0]) if value.split('/', 1)[0] else None,
date.fromisoformat(value.split('/', 1)[-1]) if value.split('/', 1)[-1] else None,
]
return [value, None, None]
def get_context(self, name, value, attrs):
ctx = super().get_context(name, value, attrs)
ctx['required'] = self.timeframe_choices[0][0] == 'unset'
ctx['widget']['subwidgets'][1]['attrs'].pop('required', None)
ctx['widget']['subwidgets'][2]['attrs'].pop('required', None)
return ctx
+1
View File
@@ -507,6 +507,7 @@ class EventSettingsForm(SettingsForm):
'meta_noindex',
'redirect_to_checkout_directly',
'frontpage_subevent_ordering',
'low_availability_percentage',
'event_list_type',
'event_list_available_only',
'frontpage_text',
+47 -2
View File
@@ -57,8 +57,8 @@ from pretix.base.forms.widgets import (
from pretix.base.models import (
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
OrderRefund, Organizer, Question, QuestionAnswer, SubEvent, Team,
TeamAPIToken, TeamInvite,
OrderRefund, Organizer, Question, QuestionAnswer, SubEvent,
SubEventMetaValue, Team, TeamAPIToken, TeamInvite,
)
from pretix.base.signals import register_payment_providers
from pretix.control.forms.widgets import Select2
@@ -1116,9 +1116,25 @@ class SubEventFilterForm(FilterForm):
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
super().__init__(*args, **kwargs)
self.fields['date_from'].widget = DatePickerWidget()
self.fields['date_until'].widget = DatePickerWidget()
for p in self.meta_properties.all():
self.fields['meta_{}'.format(p.name)] = forms.CharField(
label=p.name,
required=False,
widget=forms.TextInput(
attrs={
'data-typeahead-url': reverse('control:event.subevents.meta.typeahead', kwargs={
'organizer': self.event.organizer.slug,
'event': self.event.slug
}) + '?' + urlencode({
'property': p.name,
})
}
)
)
def filter_qs(self, qs):
fdata = self.cleaned_data
@@ -1181,6 +1197,31 @@ class SubEventFilterForm(FilterForm):
if fdata.get('time_from'):
qs = qs.filter(date_from__time__gte=fdata.get('time_from'))
filters_by_property_name = {}
for i, p in enumerate(self.meta_properties):
d = fdata.get('meta_{}'.format(p.name))
if d:
semv_with_value = SubEventMetaValue.objects.filter(
subevent=OuterRef('pk'),
property__pk=p.pk,
value=d
)
semv_with_any_value = SubEventMetaValue.objects.filter(
subevent=OuterRef('pk'),
property__pk=p.pk,
)
qs = qs.annotate(**{'attr_{}'.format(i): Exists(semv_with_value)})
if p.name in filters_by_property_name:
filters_by_property_name[p.name] |= Q(**{'attr_{}'.format(i): True})
else:
filters_by_property_name[p.name] = Q(**{'attr_{}'.format(i): True})
default = self.event.meta_data[p.name]
if default == d:
qs = qs.annotate(**{'attr_{}_any'.format(i): Exists(semv_with_any_value)})
filters_by_property_name[p.name] |= Q(**{'attr_{}_any'.format(i): False})
for f in filters_by_property_name.values():
qs = qs.filter(f)
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
else:
@@ -1188,6 +1229,10 @@ class SubEventFilterForm(FilterForm):
return qs
@cached_property
def meta_properties(self):
return self.event.organizer.meta_properties.filter(filter_allowed=True)
class OrganizerFilterForm(FilterForm):
orders = {
+1 -1
View File
@@ -564,7 +564,7 @@ class WebHookForm(forms.ModelForm):
class Meta:
model = WebHook
fields = ['target_url', 'enabled', 'all_events', 'limit_events']
fields = ['target_url', 'enabled', 'all_events', 'limit_events', 'comment']
widgets = {
'limit_events': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '#id_all_events'
+15
View File
@@ -23,6 +23,7 @@ from datetime import datetime
from urllib.parse import urlencode
from django import forms
from django.core.exceptions import ValidationError
from django.forms import formset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse
@@ -273,6 +274,13 @@ class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
'available_until': SplitDateTimeField,
}
def clean(self):
d = super().clean()
if d.get('available_from') and d.get('available_until'):
if d.get('available_from') > d.get('available_until'):
raise ValidationError(_('The end of availability should be after the start of availability.'))
return d
class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
def __init__(self, *args, **kwargs):
@@ -293,6 +301,13 @@ class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelFor
'available_until': SplitDateTimeField,
}
def clean(self):
d = super().clean()
if d.get('available_from') and d.get('available_until'):
if d.get('available_from') > d.get('available_until'):
raise ValidationError(_('The end of availability should be after the start of availability.'))
return d
class BulkSubEventItemForm(SubEventItemForm):
rel_available_from = RelativeDateTimeField(
@@ -314,7 +314,8 @@
{% if sform.event_list_available_only %}
{% bootstrap_field sform.event_list_available_only layout="control" %}
{% endif %}
{% bootstrap_field sform.low_availability_percentage layout="control" %}
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
{% propagated request.event org_url "meta_noindex" %}
{% bootstrap_field sform.meta_noindex layout="control" %}
@@ -11,6 +11,7 @@
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.target_url layout="control" %}
{% bootstrap_field form.comment layout="control" %}
{% bootstrap_field form.enabled layout="control" %}
{% bootstrap_field form.events layout="control" %}
{% bootstrap_field form.all_events layout="control" %}
@@ -90,7 +90,7 @@
</div>
</details>
{% empty %}
<div class="alert-info">{% trans "This webhook did not receive any events in the last 30 days." %}</div>
<div class="alert alert-info">{% trans "This webhook did not receive any events in the last 30 days." %}</div>
{% endfor %}
{% include "pretixcontrol/pagination.html" %}
{% endblock %}
@@ -35,6 +35,7 @@
<tr>
<th>{% trans "Target URL" %}</th>
<th>{% trans "Events" %}</th>
<th>{% trans "Comment" %}</th>
<th></th>
</tr>
</thead>
@@ -61,6 +62,11 @@
</ul>
{% endif %}
</td>
<td>
{% if w.comment %}
{{ w.comment }}
{% endif %}
</td>
<td class="text-right flip">
<a href="{% url "control:organizer.webhook.edit" organizer=request.organizer.slug webhook=w.id %}"
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Edit" %}">
@@ -275,6 +275,18 @@
<input type="number" value="13" class="input-block-level form-control" step="0.1"
id="toolbox-fontsize">
</div>
<div class="col-sm-6">
<label>{% trans "Line height" %}</label><br>
<input type="number" value="1" class="input-block-level form-control" step="0.1"
id="toolbox-lineheight">
</div>
</div>
<div class="row control-group text">
<div class="col-sm-6">
<label>{% trans "Text color" %}</label><br>
<input type="text" value="#000000" class="input-block-level form-control colorpickerfield"
id="toolbox-col">
</div>
<div class="col-sm-6">
<label>&nbsp;</label><br>
<div class="btn-group btn-group-justified" role="group">
@@ -295,15 +307,6 @@
</button>
</div>
</div>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-6">
<label>{% trans "Text color" %}</label><br>
<input type="text" value="#000000" class="input-block-level form-control colorpickerfield"
id="toolbox-col">
</div>
<div class="col-sm-6">
<label>&nbsp;</label><br>
<div class="btn-group btn-group-justified" id="toolbox-align">
<div class="btn-group" role="group">
@@ -487,6 +487,7 @@
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
</p>
{% for f in itemvar_forms %}
{% bootstrap_form_errors f %}
<div class="form-group subevent-itemvar-group">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
@@ -151,6 +151,7 @@
<fieldset>
<legend>{% trans "Item prices" %}</legend>
{% for f in itemvar_forms %}
{% bootstrap_form_errors f %}
<div class="form-group subevent-itemvar-group">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
@@ -129,6 +129,7 @@
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
</p>
{% for f in itemvar_forms %}
{% bootstrap_form_errors f %}
<div class="form-group subevent-itemvar-group">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
@@ -50,6 +50,11 @@
<div class="col-xs-12 one-line-checkboxes">
{% bootstrap_field filter_form.weekday %}
</div>
{% for mf in meta_fields %}
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field mf %}
</div>
{% endfor %}
</div>
<div class="text-right flip">
+1
View File
@@ -261,6 +261,7 @@ urlpatterns = [
re_path(r'^subevents/bulk_add$', subevents.SubEventBulkCreate.as_view(), name='event.subevents.bulk'),
re_path(r'^subevents/bulk_action$', subevents.SubEventBulkAction.as_view(), name='event.subevents.bulkaction'),
re_path(r'^subevents/bulk_edit$', subevents.SubEventBulkEdit.as_view(), name='event.subevents.bulkedit'),
re_path(r'^subevents/typeahead/meta/$', typeahead.subevent_meta_values, name='event.subevents.meta.typeahead'),
re_path(r'^items/$', item.ItemList.as_view(), name='event.items'),
re_path(r'^items/add$', item.ItemCreate.as_view(), name='event.items.add'),
re_path(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
+22 -3
View File
@@ -42,6 +42,7 @@ from itertools import groupby
from urllib.parse import urlsplit
import bleach
from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
@@ -1025,9 +1026,27 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
self.request.event.delete()
messages.success(self.request, _('The event has been deleted.'))
return redirect(self.get_success_url())
except ProtectedError:
messages.error(self.request, _('The event could not be deleted as some constraints (e.g. data created by '
'plug-ins) do not allow it.'))
except ProtectedError as e:
err = gettext('The event could not be deleted as some constraints (e.g. data created by plug-ins) do not allow it.')
app_labels = set()
for e in e.protected_objects:
app_labels.add(type(e)._meta.app_label)
plugin_names = []
for app_label in app_labels:
app = apps.get_app_config(app_label)
if hasattr(app, 'PretixPluginMeta'):
plugin_names.append(str(app.PretixPluginMeta.name))
else:
plugin_names.append(str(app.verbose_name))
if plugin_names:
err += ' ' + gettext(
'Specifically, the following plugins still contain data depends on this event: {plugin_names}'
).format(plugin_names=', '.join(plugin_names))
messages.error(self.request, err)
return self.get(self.request, *self.args, **self.kwargs)
def get_success_url(self) -> str:
+3 -1
View File
@@ -280,7 +280,9 @@ class EventWizard(SafeSessionWizardView):
elif event.organizer.settings.event_team_provisioning:
t = Team.objects.create(
organizer=event.organizer,
name=_('Team {event}').format(event=str(event.name)[:188] + "" if len(str(event.name)) > 190 else str(event.name)),
name=_('Team {event}').format(
event=str(event.name)[:100] + "" if len(str(event.name)) > 100 else str(event.name)
),
can_change_event_settings=True, can_change_items=True,
can_view_orders=True, can_change_orders=True, can_view_vouchers=True,
can_change_vouchers=True
+17 -3
View File
@@ -393,9 +393,23 @@ class OrganizerDelete(AdministratorPermissionRequiredMixin, FormView):
self.request.organizer.delete()
messages.success(self.request, _('The organizer has been deleted.'))
return redirect(self.get_success_url())
except ProtectedError:
messages.error(self.request, _('The organizer could not be deleted as some constraints (e.g. data created by '
'plug-ins) do not allow it.'))
except ProtectedError as e:
err = gettext('The organizer could not be deleted as some constraints (e.g. data created by plug-ins) do not allow it.')
# Unlike deleting events (which is done by regular backend users), this feature can only be used by sysadmins,
# so we expose more technical / less polished information.
affected_models = set()
for m in e.protected_objects:
affected_models.add(type(m)._meta.label)
if affected_models:
err += ' ' + gettext(
'The following database models still contain data that cannot be deleted automatically: {affected_models}'
).format(
affected_models=', '.join(list(affected_models))
)
messages.error(self.request, err)
return self.get(self.request, *self.args, **self.kwargs)
def get_success_url(self) -> str:
+2 -2
View File
@@ -39,8 +39,8 @@ from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import gettext as _
from django.views.generic import TemplateView
from PyPDF2 import PdfReader, PdfWriter
from PyPDF2.errors import PdfReadError
from pypdf import PdfReader, PdfWriter
from pypdf.errors import PdfReadError
from reportlab.lib.units import mm
from pretix.base.i18n import language
+4 -1
View File
@@ -110,7 +110,7 @@ class SubEventQueryMixin:
@cached_property
def filter_form(self):
return SubEventFilterForm(data=self.request_data, prefix='filter')
return SubEventFilterForm(data=self.request_data, prefix='filter', event=self.request.event)
class SubEventList(EventPermissionRequiredMixin, PaginationMixin, SubEventQueryMixin, ListView):
@@ -125,6 +125,9 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, SubEventQueryM
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form
ctx['meta_fields'] = [
self.filter_form['meta_{}'.format(p.name)] for p in self.request.organizer.meta_properties.filter(filter_allowed=True)
]
quotas = []
for s in ctx['subevents']:
+35 -2
View File
@@ -48,7 +48,8 @@ from django.utils.translation import gettext as _, pgettext
from pretix.base.models import (
EventMetaProperty, EventMetaValue, ItemMetaProperty, ItemMetaValue,
ItemVariation, ItemVariationMetaValue, Order, Organizer, User, Voucher,
ItemVariation, ItemVariationMetaValue, Order, Organizer, SubEventMetaValue,
User, Voucher,
)
from pretix.control.forms.event import EventWizardCopyForm
from pretix.control.permissions import (
@@ -556,7 +557,7 @@ def itemvarquota_select2(request, **kwargs):
if not request.event.has_subevents:
# We are very unlikely to need pagination
itemqs = request.event.items.prefetch_related('variations').filter(name__icontains=i18ncomp(query))
itemqs = request.event.items.prefetch_related('variations').filter(Q(name__icontains=i18ncomp(query)) | Q(internal_name__icontains=query))
quotaqs = request.event.quotas.filter(name__icontains=query)
more = False
else:
@@ -743,6 +744,38 @@ def meta_values(request):
})
def subevent_meta_values(request, organizer, event):
q = request.GET.get('q')
propname = request.GET.get('property')
matches = SubEventMetaValue.objects.filter(
value__icontains=q,
property__name=propname,
subevent__event_id=request.event.pk,
)
event_matches = EventMetaValue.objects.filter(
value__icontains=q,
property__name=propname,
event_id=request.event.pk,
)
defaults = EventMetaProperty.objects.filter(
default__icontains=q,
name=propname,
organizer_id=request.organizer.pk,
)
return JsonResponse({
'results': [
{'name': v, 'id': v}
for v in sorted(
set(defaults.values_list('default', flat=True)[:10]) |
set(matches.values_list('value', flat=True)[:10]) |
set(event_matches.values_list('value', flat=True)[:10])
)
]
})
def item_meta_values(request, organizer, event):
q = request.GET.get('q')
propname = request.GET.get('property')
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+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/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+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-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2023-01-13 08:00+0000\n"
"Last-Translator: tree <jan.stransky@arnal.cz>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2022-12-01 17:00+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2022-07-26 08:58+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
+3
View File
@@ -48,6 +48,7 @@ BezahlCode
Bezahlmethode
Blackberry
BLIK
Blogpost
BN
Branding
Browsereinstellungen
@@ -165,6 +166,7 @@ Linktext
loszulegen
Ltd
max
MariaDB
MapQuest
Mercado
Merchandise
@@ -206,6 +208,7 @@ Plugin
Plugins
POS
Postfix
PostgreSQL
prefix
pretix
pretixdesk
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2022-07-26 08:58+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
@@ -48,6 +48,7 @@ BezahlCode
Bezahlmethode
Blackberry
BLIK
Blogpost
BN
Branding
Browsereinstellungen
@@ -165,6 +166,7 @@ Linktext
loszulegen
Ltd
max
MariaDB
MapQuest
Mercado
Merchandise
@@ -206,6 +208,7 @@ Plugin
Plugins
POS
Postfix
PostgreSQL
prefix
pretix
pretixdesk
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+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/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+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: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2023-01-17 01:00+0000\n"
"Last-Translator: Maurice Kaag <maurice@kaag.me>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2022-02-22 22: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-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+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/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+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-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2022-05-08 19:00+0000\n"
"Last-Translator: Emanuele Signoretta <signorettae@gmail.com>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2022-03-15 00:00+0000\n"
"Last-Translator: Yuriko Matsunami <y.matsunami@enobyte.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2022-04-06 03:00+0000\n"
"Last-Translator: Liga V <lerning_by_dreaming@gmx.de>\n"
"Language-Team: Latvian <https://translate.pretix.eu/projects/pretix/pretix-"
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2022-06-20 02:00+0000\n"
"Last-Translator: fyksen <fredrik@fyksen.me>\n"
"Language-Team: Norwegian Bokmål <https://translate.pretix.eu/projects/pretix/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2021-10-29 02:00+0000\n"
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2021-08-05 04:00+0000\n"
"Last-Translator: Maarten van den Berg <maartenberg1@gmail.com>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2019-09-24 19:00+0000\n"
"Last-Translator: Serge Bazanski <q3k@hackerspace.pl>\n"
"Language-Team: Polish <https://translate.pretix.eu/projects/pretix/pretix-js/"
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-30 11:33+0000\n"
"POT-Creation-Date: 2023-02-01 13:31+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"

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