Compare commits

...

26 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 Wolkwitz
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
ser8phin
82dd417a8e Webhooks: Add comment field (#3095) 2023-02-07 15:17:19 +01:00
Richard Schreiber
ba2c6e1e58 Event index: Increase white-space for products and headlines (#3092) 2023-02-07 15:03:28 +01:00
Thomas Vranken
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 Michel
749f5c7e6c Invoices: Visibly mark paid invoices (#3094) 2023-02-07 11:34:38 +01:00
pajowu
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 Michel
4543d8093f Add webhooks for changes to items (#3087) 2023-02-06 17:52:42 +01:00
Richard Schreiber
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 Schreiber
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]
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
juliusstoerrle
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
Richard Schreiber
b4e372ce04 PDF: Add support for line height (#3066)
Co-authored-by: Raphael Michel <michel@rami.io>
2023-02-02 19:07:10 +01:00
58 changed files with 520 additions and 204 deletions

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

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": [
{

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

View File

@@ -102,6 +102,8 @@ The provider class
.. automethod:: render_invoice_text
.. automethod:: render_invoice_stamp
.. automethod:: order_change_allowed
.. automethod:: payment_prepare

View File

@@ -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),
),
]

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

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

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)

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

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

View File

@@ -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),
),
]

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)

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:
"""

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']]
)
@@ -882,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])
@@ -938,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()

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

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

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 = {

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'

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(

View File

@@ -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" %}

View File

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

View File

@@ -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" %}">

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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']:

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

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2023-02-01 13:45+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"PO-Revision-Date: 2023-02-02 20:00+0000\n"
"Last-Translator: juliusstoerrle <juliusstoerrle@gmx.de>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix/de_Informal/>\n"
"Language: de_Informal\n"
@@ -27838,7 +27838,7 @@ msgid ""
"Some of the products in your cart can only be purchased if there is an "
"active membership on your account."
msgstr ""
"Manche der Produkte in Ihrem Warenkorb kannst du nur buchen, wenn eine "
"Manche der Produkte in deinem Warenkorb kannst du nur buchen, wenn eine "
"aktive Mitgliedschaft in deinem Kundenkonto vorliegt."
#: pretix/presale/templates/pretixpresale/event/checkout_membership.html:38

View File

@@ -7,16 +7,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-01 13:31+0000\n"
"PO-Revision-Date: 2022-09-11 21:00+0000\n"
"PO-Revision-Date: 2023-02-07 10:00+0000\n"
"Last-Translator: Thomas Vranken <thvranken@gmail.com>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/"
">\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
"\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.14\n"
"X-Generator: Weblate 4.15.2\n"
#: pretix/api/auth/devicesecurity.py:28
msgid ""
@@ -149,7 +149,7 @@ msgstr ""
#: pretix/api/serializers/item.py:266
msgid "Only admission products can currently be personalized."
msgstr ""
msgstr "Momenteel kunnen enkel toegangsproducten gepersonaliseerd worden."
#: pretix/api/serializers/item.py:277
msgid ""
@@ -186,10 +186,9 @@ msgid "This type of question cannot be asked during check-in."
msgstr "Deze soort vraag kan niet bij het inchecken worden gesteld."
#: pretix/api/serializers/order.py:74
#, fuzzy, python-brace-format
#| msgid "{value} is not a valid email address."
#, python-brace-format
msgid "\"{input}\" is not a valid choice."
msgstr "{value} is geen geldig e-mailadres."
msgstr "\"{input}\" is geen geldige keuze."
#: pretix/api/serializers/order.py:1167 pretix/api/views/cart.py:210
#: pretix/base/services/orders.py:1392
@@ -15336,10 +15335,8 @@ msgstr "Server-transactiecode"
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:62
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:78
#, fuzzy
#| msgid "Payer and reference"
msgid "Payment reference"
msgstr "Betaler en kenmerk"
msgstr "Betalingsreferentie"
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:65
msgid "Payment Application"

View File

@@ -36,6 +36,7 @@ import json
import logging
from collections import OrderedDict
from datetime import datetime, time, timedelta
from decimal import Decimal
from io import BytesIO
from typing import Tuple
@@ -50,8 +51,8 @@ from django.db.models import Exists, OuterRef, Q, Subquery
from django.db.models.functions import Cast, Coalesce
from django.utils.timezone import make_aware
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
from PyPDF2 import PdfMerger, PdfReader, PdfWriter, Transformation
from PyPDF2.generic import RectangleObject
from pypdf import PdfMerger, PdfReader, PdfWriter, Transformation
from pypdf.generic import RectangleObject
from reportlab.lib import pagesizes
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
@@ -221,17 +222,17 @@ def render_pdf(event, positions, opt):
di = i % badges_per_page
if di == 0:
nup_page = nup_pdf.add_blank_page(
width=opt['pagesize'][0],
height=opt['pagesize'][1],
width=Decimal('%.5f' % (opt['pagesize'][0])),
height=Decimal('%.5f' % (opt['pagesize'][1])),
)
tx = opt['margins'][3] + (di % opt['cols']) * opt['offsets'][0]
ty = opt['margins'][2] + (opt['rows'] - 1 - (di // opt['cols'])) * opt['offsets'][1]
page.add_transformation(Transformation().translate(tx, ty))
page.mediabox = RectangleObject((
page.mediabox.left.as_numeric() + tx,
page.mediabox.bottom.as_numeric() + ty,
page.mediabox.right.as_numeric() + tx,
page.mediabox.top.as_numeric() + ty
Decimal('%.5f' % (page.mediabox.left.as_numeric() + tx)),
Decimal('%.5f' % (page.mediabox.bottom.as_numeric() + ty)),
Decimal('%.5f' % (page.mediabox.right.as_numeric() + tx)),
Decimal('%.5f' % (page.mediabox.top.as_numeric() + ty))
))
page.trimbox = page.mediabox
nup_page.merge_page(page)

View File

@@ -31,6 +31,7 @@ from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import (
Event, InvoiceAddress, Item, Order, OrderPosition, SubEvent,
)
@@ -133,44 +134,45 @@ class ScheduledMail(models.Model):
send_to_attendees = self.rule.send_to in (Rule.ATTENDEES, Rule.BOTH)
for o in orders:
positions = list(o.positions.all())
o_sent = False
with language(o.locale, e.settings.region):
positions = list(o.positions.all())
o_sent = False
try:
ia = o.invoice_address
except InvoiceAddress.DoesNotExist:
ia = InvoiceAddress(order=o)
if send_to_orders and o.email:
email_ctx = get_email_context(event=e, order=o, invoice_address=ia)
try:
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
o_sent = True
except SendMailException:
... # ¯\_(ツ)_/¯
ia = o.invoice_address
except InvoiceAddress.DoesNotExist:
ia = InvoiceAddress(order=o)
if send_to_attendees:
if not self.rule.all_products:
positions = [p for p in positions if p.item_id in limit_products]
if self.subevent_id:
positions = [p for p in positions if p.subevent_id == self.subevent_id]
for p in positions:
if send_to_orders and o.email:
email_ctx = get_email_context(event=e, order=o, invoice_address=ia)
try:
if p.attendee_email and (p.attendee_email != o.email or not o_sent):
email_ctx = get_email_context(event=e, order=o, invoice_address=ia, position=p)
p.send_mail(self.rule.subject, self.rule.template, email_ctx,
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
elif not o_sent and o.email:
email_ctx = get_email_context(event=e, order=o, invoice_address=ia)
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
o_sent = True
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
o_sent = True
except SendMailException:
... # ¯\_(ツ)_/¯
self.last_successful_order_id = o.pk
if send_to_attendees:
if not self.rule.all_products:
positions = [p for p in positions if p.item_id in limit_products]
if self.subevent_id:
positions = [p for p in positions if p.subevent_id == self.subevent_id]
for p in positions:
try:
if p.attendee_email and (p.attendee_email != o.email or not o_sent):
email_ctx = get_email_context(event=e, order=o, invoice_address=ia, position=p)
p.send_mail(self.rule.subject, self.rule.template, email_ctx,
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
elif not o_sent and o.email:
email_ctx = get_email_context(event=e, order=o, invoice_address=ia)
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
o_sent = True
except SendMailException:
... # ¯\_(ツ)_/¯
self.last_successful_order_id = o.pk
class Rule(models.Model, LoggingMixin):

View File

@@ -43,7 +43,7 @@ from django.db.models import OuterRef, Q, Subquery
from django.db.models.functions import Cast, Coalesce
from django.utils.timezone import now
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
from PyPDF2 import PdfMerger
from pypdf import PdfMerger
from pretix.base.exporter import BaseExporter
from pretix.base.i18n import language

View File

@@ -44,7 +44,7 @@ from django.http import HttpRequest
from django.template.loader import get_template
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from PyPDF2 import PdfMerger
from pypdf import PdfMerger
from pretix.base.i18n import language
from pretix.base.models import Order, OrderPosition

View File

@@ -16,7 +16,7 @@
<span role="columnheader" aria-sort="none">{% trans "Price total" %}</span>
</div>
</div>
<div role="rowgroup">
<div role="rowgroup" class="firstchild-in-panel">
{% for line in cart.positions %}
<div role="row" class="row cart-row {% if download %}has-downloads{% endif %}{% if editable %}editable{% endif %}">
<div role="cell" class="product">

View File

@@ -8,7 +8,7 @@
"name": "pretix",
"version": "0.0.0",
"dependencies": {
"@babel/core": "^7.20.7",
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.0.1",
@@ -50,24 +50,24 @@
}
},
"node_modules/@babel/core": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.7.tgz",
"integrity": "sha512-t1ZjCluspe5DW24bn2Rr1CDb2v9rn/hROtg9a2tmd0+QYf4bsloYfLQzjG4qHPNMhWtKdGC33R5AxGR2Af2cBw==",
"version": "7.20.12",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz",
"integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==",
"dependencies": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.7",
"@babel/helper-compilation-targets": "^7.20.7",
"@babel/helper-module-transforms": "^7.20.7",
"@babel/helper-module-transforms": "^7.20.11",
"@babel/helpers": "^7.20.7",
"@babel/parser": "^7.20.7",
"@babel/template": "^7.20.7",
"@babel/traverse": "^7.20.7",
"@babel/traverse": "^7.20.12",
"@babel/types": "^7.20.7",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.2.1",
"json5": "^2.2.2",
"semver": "^6.3.0"
},
"engines": {
@@ -79,9 +79,9 @@
}
},
"node_modules/@babel/core/node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": {
"json5": "lib/cli.js"
},
@@ -475,9 +475,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz",
"integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==",
"version": "7.20.13",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz",
"integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==",
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -1541,9 +1541,9 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.20.10",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.10.tgz",
"integrity": "sha512-oSf1juCgymrSez8NI4A2sr4+uB/mFd9MXplYGPEBnfAuWmmyeVcHa6xLPiaRBcXkcb/28bgxmQLTVwFKE1yfsg==",
"version": "7.20.13",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz",
"integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==",
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.7",
@@ -1551,7 +1551,7 @@
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.20.7",
"@babel/parser": "^7.20.13",
"@babel/types": "^7.20.7",
"debug": "^4.1.0",
"globals": "^11.1.0"
@@ -4063,31 +4063,31 @@
"integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg=="
},
"@babel/core": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.7.tgz",
"integrity": "sha512-t1ZjCluspe5DW24bn2Rr1CDb2v9rn/hROtg9a2tmd0+QYf4bsloYfLQzjG4qHPNMhWtKdGC33R5AxGR2Af2cBw==",
"version": "7.20.12",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz",
"integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==",
"requires": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.7",
"@babel/helper-compilation-targets": "^7.20.7",
"@babel/helper-module-transforms": "^7.20.7",
"@babel/helper-module-transforms": "^7.20.11",
"@babel/helpers": "^7.20.7",
"@babel/parser": "^7.20.7",
"@babel/template": "^7.20.7",
"@babel/traverse": "^7.20.7",
"@babel/traverse": "^7.20.12",
"@babel/types": "^7.20.7",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.2.1",
"json5": "^2.2.2",
"semver": "^6.3.0"
},
"dependencies": {
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
},
"semver": {
"version": "6.3.0",
@@ -4378,9 +4378,9 @@
}
},
"@babel/parser": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz",
"integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg=="
"version": "7.20.13",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz",
"integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw=="
},
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
"version": "7.18.6",
@@ -5073,9 +5073,9 @@
}
},
"@babel/traverse": {
"version": "7.20.10",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.10.tgz",
"integrity": "sha512-oSf1juCgymrSez8NI4A2sr4+uB/mFd9MXplYGPEBnfAuWmmyeVcHa6xLPiaRBcXkcb/28bgxmQLTVwFKE1yfsg==",
"version": "7.20.13",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz",
"integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==",
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.7",
@@ -5083,7 +5083,7 @@
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.20.7",
"@babel/parser": "^7.20.13",
"@babel/types": "^7.20.7",
"debug": "^4.1.0",
"globals": "^11.1.0"

View File

@@ -4,7 +4,7 @@
"private": true,
"scripts": {},
"dependencies": {
"@babel/core": "^7.20.7",
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.0.1",

View File

@@ -119,6 +119,7 @@ $(document).ready(function () {
date_tolerance: gettext('Tolerance (minutes)'),
condition_add: gettext('Add condition'),
minutes: gettext('minutes'),
duplicate: gettext('Duplicate'),
},
hasRules: false,
};

View File

@@ -1,6 +1,10 @@
<template>
<div v-bind:class="classObject">
<div class="btn-group pull-right">
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="duplicate"
v-if="level > 0" data-toggle="tooltip" :title="texts.duplicate">
<span class="fa fa-copy"></span>
</button>
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="wrapWithOR">OR
</button>
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="wrapWithAND">AND
@@ -260,6 +264,10 @@
remove: function () {
this.$parent.rule[this.$parent.operator].splice(this.index, 1);
},
duplicate: function () {
var r = JSON.parse(JSON.stringify(this.rule));
this.$parent.rule[this.$parent.operator].splice(this.index, 0, r);
},
}
}
</script>

View File

@@ -10,25 +10,29 @@
' <slot></slot>\n' +
' </select>'),
mounted: function () {
var vm = this;
var multiple = this.multiple;
$(this.$el)
.select2(this.opts())
.val(this.value || "")
.trigger("change")
// emit event on change.
.on("change", function (e) {
vm.$emit("input", $(this).select2('data'));
});
if (vm.value) {
for (var i = 0; i < vm.value["objectList"].length; i++) {
var option = new Option(vm.value["objectList"][i]["lookup"][2], vm.value["objectList"][i]["lookup"][1], true, true);
$(vm.$el).append(option);
}
}
$(vm.$el).trigger("change");
this.build();
},
methods: {
build: function () {
var vm = this;
var multiple = this.multiple;
$(this.$el)
.empty()
.select2(this.opts())
.val(this.value || "")
.trigger("change")
// emit event on change.
.on("change", function (e) {
vm.$emit("input", $(this).select2('data'));
});
if (vm.value) {
for (var i = 0; i < vm.value["objectList"].length; i++) {
var option = new Option(vm.value["objectList"][i]["lookup"][2], vm.value["objectList"][i]["lookup"][1], true, true);
$(vm.$el).append(option);
}
}
$(vm.$el).trigger("change");
},
opts: function () {
return {
theme: "bootstrap",
@@ -60,17 +64,29 @@
},
watch: {
placeholder: function (val) {
$(this.$el).empty().select2(this.opts());
$(this.$el).select2("destroy");
this.build();
},
required: function (val) {
$(this.$el).empty().select2(this.opts());
$(this.$el).select2("destroy");
this.build();
},
url: function (val) {
$(this.$el).empty().select2(this.opts());
$(this.$el).select2("destroy");
this.build();
},
value: function (newval, oldval) {
if (JSON.stringify(newval) !== JSON.stringify(oldval)) {
$(this.$el).empty();
if (newval) {
for (var i = 0; i < newval["objectList"].length; i++) {
var option = new Option(newval["objectList"][i]["lookup"][2], newval["objectList"][i]["lookup"][1], true, true);
$(this.$el).append(option);
}
}
$(this.$el).trigger("change");
}
},
},
destroyed: function () {
$(this.$el)

View File

@@ -161,8 +161,8 @@ var editor = {
left: editor._px2mm(left).toFixed(2),
bottom: editor._px2mm(bottom).toFixed(2),
fontsize: editor._px2pt(o.getFontSize()).toFixed(1),
lineheight: o.lineHeight,
color: col,
//lineheight: o.lineHeight,
fontfamily: o.fontFamily,
bold: o.fontWeight === 'bold',
italic: o.fontStyle === 'italic',
@@ -241,7 +241,7 @@ var editor = {
o = editor._add_text();
o.setColor('rgb(' + d.color[0] + ',' + d.color[1] + ',' + d.color[2] + ')');
o.setFontSize(editor._pt2px(d.fontsize));
//o.setLineHeight(d.lineheight);
o.setLineHeight(d.lineheight || 1);
o.setFontFamily(d.fontfamily);
o.setFontWeight(d.bold ? 'bold' : 'normal');
o.setFontStyle(d.italic ? 'italic' : 'normal');
@@ -483,7 +483,7 @@ var editor = {
var col = (new fabric.Color(o.getFill()))._source;
$("#toolbox-col").val("#" + ((1 << 24) + (col[0] << 16) + (col[1] << 8) + col[2]).toString(16).slice(1));
$("#toolbox-fontsize").val(editor._px2pt(o.fontSize).toFixed(1));
//$("#toolbox-lineheight").val(o.lineHeight);
$("#toolbox-lineheight").val(o.lineHeight || 1);
$("#toolbox-fontfamily").val(o.fontFamily);
$("#toolbox").find("button[data-action=bold]").toggleClass('active', o.fontWeight === 'bold');
$("#toolbox").find("button[data-action=italic]").toggleClass('active', o.fontStyle === 'italic');
@@ -600,7 +600,7 @@ var editor = {
} else if (o.type === "textarea" || o.type === "text") {
o.setColor($("#toolbox-col").val());
o.setFontSize(editor._pt2px($("#toolbox-fontsize").val()));
//o.setLineHeight($("#toolbox-lineheight").val());
o.setLineHeight($("#toolbox-lineheight").val() || 1);
o.setFontFamily($("#toolbox-fontfamily").val());
o.setFontWeight($("#toolbox").find("button[data-action=bold]").is('.active') ? 'bold' : 'normal');
o.setFontStyle($("#toolbox").find("button[data-action=italic]").is('.active') ? 'italic' : 'normal');
@@ -861,6 +861,7 @@ var editor = {
thing.setCoords();
editor._create_savepoint();
break;
case 8: /* Backspace */
case 46: /* Delete */
editor._delete();
break;

View File

@@ -6,7 +6,18 @@ $(function () {
}
function update_variation_summary($el) {
var var_name = $el.find("input[name*=-value_]").filter(function () {return !!this.value}).first().val();
var var_names = Object.fromEntries(
$el
.find("input[name*=-value_]")
.filter(function () {
return !!this.value;
})
.map(function () {
return [[this.getAttribute("lang"), this.value]];
})
.get()
);
var var_name = i18nToString(var_names);
var price = $el.find("input[name*=-default_price]").val();
if (price) {
var currency = $el.find("[name*=-default_price] + .input-group-addon").text();

View File

@@ -362,7 +362,7 @@ Vue.component('pricebox', {
}
});
Vue.component('variation', {
template: ('<div class="pretix-widget-variation">'
template: ('<div class="pretix-widget-variation" :data-id="variation.id">'
+ '<div class="pretix-widget-item-row">'
// Variation description
@@ -411,7 +411,7 @@ Vue.component('variation', {
}
});
Vue.component('item', {
template: ('<div v-bind:class="classObject">'
template: ('<div v-bind:class="classObject" :data-id="item.id">'
+ '<div class="pretix-widget-item-row pretix-widget-main-item-row">'
// Product description
@@ -517,7 +517,7 @@ Vue.component('item', {
}
});
Vue.component('category', {
template: ('<div class="pretix-widget-category">'
template: ('<div class="pretix-widget-category" :data-id="category.id">'
+ '<h3 class="pretix-widget-category-name" v-if="category.name">{{ category.name }}</h3>'
+ '<div class="pretix-widget-category-description" v-if="category.description" v-html="category.description">'
+ '</div>'

View File

@@ -1,6 +1,6 @@
.cart-row {
/* Cart grid */
padding: 10px 0;
padding: 0.75*$line-height-computed 0;
.product>*:last-child {
margin-bottom: 0;
@@ -100,6 +100,9 @@
}
}
.cart .firstchild-in-panel .cart-row {
padding-top: 0;
}
.cart [role=rowgroup]:last-child {
border-top: 1px solid $table-border-color;
}

View File

@@ -96,9 +96,9 @@
}
&.headline, &.simple {
border-top: 2px solid transparent;
border-top: 1px solid transparent;
&::before {
border-top: 2px solid $table-border-color;
border-top: 1px solid $table-border-color;
}
}
}
@@ -110,14 +110,14 @@ article.item-with-variations {
article.item-with-variations:last-child, .product-row:last-child {
position: relative;
border-bottom: 2px solid transparent;
border-bottom: 1px solid transparent;
&::after {
position: absolute;
bottom: 0;
left: 15px;
width: calc(100% - 30px);
content: '';
border-bottom: 2px solid $table-border-color;
border-bottom: 1px solid $table-border-color;
}
}
article.item-with-variations .product-row:last-child:after {
@@ -146,7 +146,7 @@ article.item-with-variations .product-row:last-child:after {
}
.product-row {
padding: 10px 0;
padding: 0.75*$line-height-computed 0;
.count form {
display: inline;

View File

@@ -1,5 +1,12 @@
@import "../../pretixbase/scss/_contrast.scss";
h1, .h1,
h2, .h2,
h3, .h3 {
margin-top: 2.25 * $line-height-computed;
margin-bottom: 0.75 * $line-height-computed;
}
.page-header {
position: relative;
padding-bottom: 9px;
@@ -150,4 +157,4 @@ footer {
border-top-left-radius: .25em;
border-bottom-left-radius: .25em;
}
}
}

View File

@@ -351,8 +351,8 @@ body.loading .container {
.panel-title a[data-toggle="collapse"], details .panel-title {
display: flex;
padding: 10px 15px;
margin: -10px -15px;
padding: 0.75*$line-height-computed;
margin: -0.75*$line-height-computed;
align-items: center;
justify-content: space-between;
outline: 0;

View File

@@ -240,7 +240,19 @@
"pattern": "[a-zA-Z-]*"
},
"fontsize": {
"description": "Font size.",
"description": "Font size",
"oneOf": [
{
"type": "number"
},
{
"type": "string",
"pattern": "^[0-9]+(\\.[0-9]+)?$"
}
]
},
"lineheight": {
"description": "Line height",
"oneOf": [
{
"type": "number"

View File

@@ -200,7 +200,7 @@ setup(
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
'mt-940==4.23.*',
'oauthlib==3.2.*',
'openpyxl==3.0.*',
'openpyxl==3.1.*',
'packaging',
'paypalrestsdk==1.13.*',
'paypal-checkout-serversdk==1.0.*',
@@ -211,8 +211,8 @@ setup(
'psycopg2-binary',
'pycountry',
'pycparser==2.21',
'pycryptodome==3.16.*',
'PyPDF2==3.0.*',
'pycryptodome==3.17.*',
'pypdf==3.4.*',
'python-bidi==0.4.*', # Support for Arabic in reportlab
'python-dateutil==2.8.*',
'python-u2flib-server==4.*',
@@ -225,7 +225,7 @@ setup(
'sepaxml==2.6.*',
'slimit',
'static3==0.7.*',
'stripe==5.0.*',
'stripe==5.1.*',
'text-unidecode==1.*',
'tlds>=2020041600',
'tqdm==4.*',

View File

@@ -176,6 +176,7 @@ TEST_INVOICE_RES = {
"internal_reference": "",
"additional_text": "",
"payment_provider_text": "",
"payment_provider_stamp": None,
"footer_text": "",
"foreign_currency_display": None,
"foreign_currency_rate": None,

View File

@@ -445,6 +445,7 @@ def test_order_create_invoice(token_client, organizer, event, order):
'introductory_text': '',
'additional_text': '',
'payment_provider_text': '',
'payment_provider_stamp': None,
'footer_text': '',
'lines': [
{

View File

@@ -47,6 +47,7 @@ TEST_WEBHOOK_RES = {
"all_events": False,
"limit_events": ['dummy'],
"action_types": ['pretix.event.order.paid', 'pretix.event.order.placed'],
"comment": None,
}

View File

@@ -39,7 +39,7 @@ from io import BytesIO
import pytest
from django.utils.timezone import now
from django_scopes import scope
from PyPDF2 import PdfReader
from pypdf import PdfReader
from pretix.base.models import (
Event, Item, ItemVariation, Order, OrderPosition, Organizer,

View File

@@ -28,7 +28,7 @@ from django.core import mail as djmail
from django.utils.timezone import now, utc
from django_scopes import scopes_disabled
from pretix.base.models import Order
from pretix.base.models import InvoiceAddress, Order
from pretix.plugins.sendmail.models import Rule, ScheduledMail
from pretix.plugins.sendmail.signals import sendmail_run_rules
@@ -361,3 +361,22 @@ def test_sendmail_rule_disabled(event, order):
sendmail_run_rules(None)
assert len(djmail.outbox) == 0
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_context_localization(event, order, pos):
order.locale = 'de'
order.save()
event.settings.name_scheme = 'salutation_given_family'
InvoiceAddress.objects.create(
order=order,
name_parts={'_scheme': 'salutation_given_family', 'salutation': 'Mr', 'given_name': 'Max', 'family_name': 'Mustermann'}
)
djmail.outbox = []
event.sendmail_rules.create(send_date=dt_now - datetime.timedelta(hours=1), include_pending=True,
subject='meow', template='Hallo {name_for_salutation}')
sendmail_run_rules(None)
assert "Hallo Herr Mustermann" in djmail.outbox[0].body

View File

@@ -26,7 +26,7 @@ from io import BytesIO
import pytest
from django.utils.timezone import now
from django_scopes import scope
from PyPDF2 import PdfReader
from pypdf import PdfReader
from pretix.base.models import (
Event, Item, ItemVariation, Order, OrderPosition, Organizer,