[SECURITY] Properly escape HTML tags in PDF generation (CVE-2026-57535)

This commit is contained in:
Raphael Michel
2026-06-25 16:21:44 +02:00
parent 257bb2e4b7
commit 8ec81f80e8
7 changed files with 239 additions and 213 deletions
+76 -90
View File
@@ -22,9 +22,7 @@
import datetime import datetime
import logging import logging
import math import math
import re
import textwrap import textwrap
import unicodedata
from collections import defaultdict from collections import defaultdict
from decimal import Decimal from decimal import Decimal
from io import BytesIO from io import BytesIO
@@ -58,8 +56,8 @@ from pretix.base.services.currencies import SOURCE_NAMES
from pretix.base.signals import register_invoice_renderers from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import ( from pretix.helpers.reportlab import (
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new, FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader,
reshaper, normalize_text, register_ttf_font_if_new, reshaper,
) )
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -259,18 +257,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype'])) register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
def _normalize(self, text): def _normalize(self, text):
# reportlab does not support unicode combination characters # alias kept for plugin compatibility
# It's important we do this before we use ArabicReshaper return normalize_text(text)
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:
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
def _upper(self, val): def _upper(self, val):
# We uppercase labels, but not in every language # We uppercase labels, but not in every language
@@ -351,10 +339,15 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read() return 'invoice.pdf', 'application/pdf', buffer.read()
def _clean_text(self, text, tags=None): def _clean_text(self, text, tags=None):
return self._normalize(bleach.clean( # For backwards compatibility with customer content, we need to support tags like <br> and <b> in a few text
text, # fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed
tags=set(tags) if tags else set() # tags.
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')) return self._normalize(
bleach.clean(
text,
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
)
class PaidMarker(Flowable): class PaidMarker(Flowable):
@@ -405,8 +398,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_to_top = 52 * mm invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas): def _draw_invoice_to(self, canvas):
p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to), p = PlainTextParagraph(self.invoice.address_invoice_to, style=self.stylesheet['Normal'])
style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height) p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height) p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top) p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
@@ -417,8 +409,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_from_top = 17 * mm invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas): def _draw_invoice_from(self, canvas):
p = FontFallbackParagraph( p = PlainTextParagraph(
self._clean_text(self.invoice.full_invoice_from), self.invoice.full_invoice_from,
style=self.stylesheet['InvoiceFrom'] style=self.stylesheet['InvoiceFrom']
) )
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height) p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
@@ -548,13 +540,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event(self, canvas): def _draw_event(self, canvas):
def shorten(txt): def shorten(txt):
txt = str(txt) txt = str(txt)
txt = bleach.clean(txt, tags=set()).strip() p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height) p_size = p.wrap(self.event_width, self.event_height)
while p_size[1] > 2 * self.stylesheet['Normal'].leading: while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('', '').split()[:-1]) + '' txt = ' '.join(txt.replace('', '').split()[:-1]) + ''
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal']) p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height) p_size = p.wrap(self.event_width, self.event_height)
return txt return txt
@@ -572,7 +563,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
else: else:
p_str = shorten(self.invoice.event.name) p_str = shorten(self.invoice.event.name)
p = FontFallbackParagraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal']) p = PlainTextParagraph(p_str, style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.event_width, self.event_height) p.wrapOn(canvas, self.event_width, self.event_height)
p_size = p.wrap(self.event_width, self.event_height) p_size = p.wrap(self.event_width, self.event_height)
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1]) p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
@@ -645,39 +636,37 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
type_info_text = self.invoice.transmission_type_instance.pdf_info_text() type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
if type_info_text: if type_info_text:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
type_info_text, type_info_text,
self.stylesheet['WarningBlock'] self.stylesheet['WarningBlock']
)) ))
if self.invoice.custom_field: if self.invoice.custom_field:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
'{}: {}'.format( '{}: {}'.format(
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)), str(self.invoice.event.settings.invoice_address_custom_field),
self._clean_text(self.invoice.custom_field), self.invoice.custom_field,
), ),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.internal_reference: if self.invoice.internal_reference:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format( pgettext('invoice', 'Customer reference: {reference}').format(
reference=self._clean_text(self.invoice.internal_reference), reference=self.invoice.internal_reference,
)), ),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.invoice_to_vat_id: if self.invoice.invoice_to_vat_id:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' + pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id,
self._clean_text(self.invoice.invoice_to_vat_id),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.invoice_to_beneficiary: if self.invoice.invoice_to_beneficiary:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' + pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary,
self._clean_text(self.invoice.invoice_to_beneficiary),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
@@ -707,11 +696,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [ story = [
NextPageTemplate('FirstPage'), NextPageTemplate('FirstPage'),
FontFallbackParagraph( PlainTextParagraph(
self._normalize( (
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU' pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
else pgettext('invoice', 'Invoice') else pgettext('invoice', 'Invoice')
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')), ) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
self.stylesheet['Heading1'] self.stylesheet['Heading1']
), ),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
@@ -733,17 +722,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
] ]
if has_taxes: if has_taxes:
tdata = [( tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']), PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']),
)] )]
else: else:
tdata = [( tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']), PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']),
)] )]
def _group_key(line): def _group_key(line):
@@ -780,8 +769,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
max_height = self.stylesheet['Normal'].leading * 5 max_height = self.stylesheet['Normal'].leading * 5
p_style = self.stylesheet['Normal'] p_style = self.stylesheet['Normal']
for __ in range(1000): for __ in range(1000):
p = FontFallbackParagraph( p = PlainTextParagraph(
self._clean_text(curr_description, tags=['br']), curr_description,
p_style p_style
) )
h = p.wrap(max_width, doc.height)[1] h = p.wrap(max_width, doc.height)[1]
@@ -862,7 +851,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
# Group together at the end of the invoice # Group together at the end of the invoice
request_show_service_date = period_line request_show_service_date = period_line
elif period_line: elif period_line:
description_p_list.append(FontFallbackParagraph( description_p_list.append(PlainTextParagraph(
period_line, period_line,
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
@@ -874,7 +863,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net_price=money_filter(net_value, self.invoice.event.currency), net_price=money_filter(net_value, self.invoice.event.currency),
gross_price=money_filter(gross_value, self.invoice.event.currency), gross_price=money_filter(gross_value, self.invoice.event.currency),
) )
description_p_list.append(FontFallbackParagraph( description_p_list.append(PlainTextParagraph(
single_price_line, single_price_line,
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
@@ -883,11 +872,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
description_p_list.pop(0), description_p_list.pop(0),
str(len(lines)), str(len(lines)),
localize(tax_rate) + " %", localize(tax_rate) + " %",
FontFallbackParagraph( PlainTextParagraph(
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
FontFallbackParagraph( PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
@@ -904,14 +893,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
single_price_line = pgettext('invoice', 'Single price: {price}').format( single_price_line = pgettext('invoice', 'Single price: {price}').format(
price=money_filter(gross_value, self.invoice.event.currency), price=money_filter(gross_value, self.invoice.event.currency),
) )
description_p_list.append(FontFallbackParagraph( description_p_list.append(PlainTextParagraph(
single_price_line, single_price_line,
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
tdata.append(( tdata.append((
description_p_list.pop(0), description_p_list.pop(0),
str(len(lines)), str(len(lines)),
FontFallbackParagraph( PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
@@ -944,12 +933,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes: if has_taxes:
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '', PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency) money_filter(total, self.invoice.event.currency)
]) ])
else: else:
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency) money_filter(total, self.invoice.event.currency)
]) ])
@@ -958,12 +947,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
pending_sum = self.invoice.order.pending_sum pending_sum = self.invoice.order.pending_sum
if pending_sum != total: if pending_sum != total:
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] + [PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum - total, self.invoice.event.currency)] [money_filter(pending_sum - total, self.invoice.event.currency)]
) )
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] + [PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum, self.invoice.event.currency)] [money_filter(pending_sum, self.invoice.event.currency)]
) )
@@ -980,12 +969,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
s=Sum('amount') s=Sum('amount')
)['s'] or Decimal('0.00') )['s'] or Decimal('0.00')
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] + [PlainTextParagraph(pgettext('invoice', 'Paid by gift card'), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(giftcard_sum, self.invoice.event.currency)] [money_filter(giftcard_sum, self.invoice.event.currency)]
) )
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] + [PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(total - giftcard_sum, self.invoice.event.currency)] [money_filter(total - giftcard_sum, self.invoice.event.currency)]
) )
@@ -1008,14 +997,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(Spacer(1, 10 * mm)) story.append(Spacer(1, 10 * mm))
if request_show_service_date: if request_show_service_date:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)), pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.payment_provider_text: if self.invoice.payment_provider_text:
story.append(FontFallbackParagraph( story.append(FontFallbackParagraph(
self._normalize(self.invoice.payment_provider_text), self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
@@ -1039,10 +1028,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('FONTNAME', (0, 0), (-1, -1), self.font_regular), ('FONTNAME', (0, 0), (-1, -1), self.font_regular),
] ]
thead = [ thead = [
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']), PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']), PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']), PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']), PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']),
'' ''
] ]
tdata = [thead] tdata = [thead]
@@ -1053,7 +1042,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
continue continue
tax = taxvalue_map[idx] tax = taxvalue_map[idx]
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']), PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
money_filter(gross - tax, self.invoice.event.currency), money_filter(gross - tax, self.invoice.event.currency),
money_filter(gross, self.invoice.event.currency), money_filter(gross, self.invoice.event.currency),
money_filter(tax, self.invoice.event.currency), money_filter(tax, self.invoice.event.currency),
@@ -1072,7 +1061,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata)) table.setStyle(TableStyle(tstyledata))
story.append(Spacer(5 * mm, 5 * mm)) story.append(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([ story.append(KeepTogether([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']), PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
table table
])) ]))
@@ -1089,7 +1078,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net = gross - tax net = gross - tax
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']), PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
fmt(net), fmt(gross), fmt(tax), '' fmt(net), fmt(gross), fmt(tax), ''
]) ])
@@ -1098,13 +1087,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(KeepTogether([ story.append(KeepTogether([
Spacer(1, height=2 * mm), Spacer(1, height=2 * mm),
FontFallbackParagraph( PlainTextParagraph(
self._normalize(pgettext( pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on ' 'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, this corresponds to:' '{date}, this corresponds to:'
).format(rate=localize(self.invoice.foreign_currency_rate), ).format(rate=localize(self.invoice.foreign_currency_rate),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"), authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))), date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
), ),
Spacer(1, height=3 * mm), Spacer(1, height=3 * mm),
@@ -1113,14 +1102,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate: elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate) foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm)) story.append(Spacer(1, 5 * mm))
story.append(FontFallbackParagraph(self._normalize( story.append(PlainTextParagraph(
pgettext( pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on ' 'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, the invoice total corresponds to {total}.' '{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate), ).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"), date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"), authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
total=fmt(foreign_total))), total=fmt(foreign_total)),
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
@@ -1162,11 +1151,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _draw_invoice_from(self, canvas): def _draw_invoice_from(self, canvas):
if not self.invoice.invoice_from: if not self.invoice.invoice_from:
return return
c = [ c = self.invoice.address_invoice_from.strip().split('\n')
self._clean_text(l) p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender'])
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm) p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm) p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
super()._draw_invoice_from(canvas) super()._draw_invoice_from(canvas)
@@ -1225,8 +1211,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs) _draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
] ]
p = FontFallbackParagraph( p = PlainTextParagraph(
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")), date_format(self.invoice.date, "DATE_FORMAT"),
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2) style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
) )
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize) w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
@@ -1283,7 +1269,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
i = [] i = []
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage: if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
i.append(FontFallbackParagraph( i.append(PlainTextParagraph(
pgettext('invoice', 'Event date: {date_range}').format( pgettext('invoice', 'Event date: {date_range}').format(
date_range=self.invoice.event.get_date_range_display(), date_range=self.invoice.event.get_date_range_display(),
), ),
+1 -1
View File
@@ -1045,7 +1045,7 @@ class Renderer:
except: except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text))) logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
p = Paragraph(text, style=style) p = Paragraph(text, style=style) # not using AutoEscapeParagraph is safe as we escape above
return p, ad, lineheight return p, ad, lineheight
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict): def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
+23 -22
View File
@@ -38,6 +38,7 @@ from pretix import __version__
from pretix.base.models import Order, OrderPayment, Transaction from pretix.base.models import Order, OrderPayment, Transaction
from pretix.base.plugins import get_all_plugins from pretix.base.plugins import get_all_plugins
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import PlainTextParagraph
from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.plugins.reports.exporters import ReportlabExportMixin
from pretix.settings import DATA_DIR from pretix.settings import DATA_DIR
@@ -79,23 +80,23 @@ class SysReport(ReportlabExportMixin):
style_small.fontSize = 6 style_small.fontSize = 6
story = [ story = [
Paragraph("System report", headlinestyle), PlainTextParagraph("System report", headlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Usage", subheadlinestyle), PlainTextParagraph("Usage", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
self._usage_table(), self._usage_table(),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Installed versions", subheadlinestyle), PlainTextParagraph("Installed versions", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
self._tech_table(), self._tech_table(),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Plugins", subheadlinestyle), PlainTextParagraph("Plugins", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph(self._get_plugin_versions(), style_small), PlainTextParagraph(self._get_plugin_versions(), style_small),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Custom templates", subheadlinestyle), PlainTextParagraph("Custom templates", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph(self._get_custom_templates(), style_small), PlainTextParagraph(self._get_custom_templates(), style_small),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
] ]
@@ -121,13 +122,13 @@ class SysReport(ReportlabExportMixin):
("RIGHTPADDING", (-1, 0), (-1, -1), 0), ("RIGHTPADDING", (-1, 0), (-1, -1), 0),
] ]
tdata = [ tdata = [
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)], [PlainTextParagraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[Paragraph("pretix version:", style), Paragraph(__version__, style)], [PlainTextParagraph("pretix version:", style), Paragraph(__version__, style)],
[Paragraph("Python version:", style), Paragraph(sys.version, style)], [PlainTextParagraph("Python version:", style), Paragraph(sys.version, style)],
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)], [PlainTextParagraph("Platform:", style), Paragraph(platform.platform(), style)],
[ [
Paragraph("Database engine:", style), PlainTextParagraph("Database engine:", style),
Paragraph(settings.DATABASES["default"]["ENGINE"], style), PlainTextParagraph(settings.DATABASES["default"]["ENGINE"], style),
], ],
] ]
table = Table(tdata, colWidths=colwidths, repeatRows=0) table = Table(tdata, colWidths=colwidths, repeatRows=0)
@@ -206,7 +207,7 @@ class SysReport(ReportlabExportMixin):
year_last = now().year year_last = now().year
tdata = [ tdata = [
[ [
Paragraph(l, style_small_head) PlainTextParagraph(l, style_small_head)
for l in ( for l in (
"Time frame", "Time frame",
"Currency", "Currency",
@@ -257,19 +258,19 @@ class SysReport(ReportlabExportMixin):
tdata.append( tdata.append(
( (
Paragraph( PlainTextParagraph(
date_format(first_day, "M Y") date_format(first_day, "M Y")
+ " " + " "
+ date_format(after_day - timedelta(days=1), "M Y"), + date_format(after_day - timedelta(days=1), "M Y"),
style_small, style_small,
), ),
Paragraph(c, style_small), PlainTextParagraph(c, style_small),
Paragraph(str(orders_count), style_small) if i == 0 else "", PlainTextParagraph(str(orders_count), style_small) if i == 0 else "",
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small), PlainTextParagraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
Paragraph(str(testmode_count), style_small) if i == 0 else "", PlainTextParagraph(str(testmode_count), style_small) if i == 0 else "",
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "", PlainTextParagraph(str(unconfirmed_count), style_small) if i == 0 else "",
Paragraph(str(revenue_data.get("c") or 0), style_small), PlainTextParagraph(str(revenue_data.get("c") or 0), style_small),
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small), PlainTextParagraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
) )
) )
+39
View File
@@ -20,14 +20,19 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import logging import logging
import re
import unicodedata
from arabic_reshaper import ArabicReshaper from arabic_reshaper import ArabicReshaper
from bidi import get_display
from django.conf import settings from django.conf import settings
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from django.utils.html import escape
from PIL import Image from PIL import Image
from reportlab.lib.styles import ParagraphStyle from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.utils import ImageReader from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import Paragraph from reportlab.platypus import Paragraph
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -70,6 +75,20 @@ reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
})) }))
def normalize_text(text: str) -> str:
# 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:
text = "\n".join(get_display(reshaper.reshape(l)) for l in re.split("\n", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
class FontFallbackParagraph(Paragraph): class FontFallbackParagraph(Paragraph):
def __init__(self, text, style=None, *args, **kwargs): def __init__(self, text, style=None, *args, **kwargs):
if style is None: if style is None:
@@ -87,6 +106,8 @@ class FontFallbackParagraph(Paragraph):
if not text: if not text:
return True return True
font = pdfmetrics.getFont(font_name) font = pdfmetrics.getFont(font_name)
if not isinstance(font, TTFont):
return True
return all( return all(
ord(c) in font.face.charToGlyph or not c.isprintable() ord(c) in font.face.charToGlyph or not c.isprintable()
for c in text for c in text
@@ -102,6 +123,24 @@ class FontFallbackParagraph(Paragraph):
return family return family
class PlainTextParagraph(FontFallbackParagraph):
def __init__(self, text, style=None, linebreaks=True, *args, **kwargs):
if not isinstance(text, str):
if hasattr(text, '__html__'):
raise ValueError("It is contradictory to pass escaped content to PlainTextParagraph")
text = str(text)
# Normalize unicode and apply reshaping
text = normalize_text(text)
# Escape any HTML in the text
text = escape(text)
if linebreaks:
text = text.strip().replace("\n", "<br />\n")
super().__init__(text, style, *args, **kwargs)
def register_ttf_font_if_new(name, path): def register_ttf_font_if_new(name, path):
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfbase.ttfonts import TTFont
+9 -9
View File
@@ -64,7 +64,7 @@ from pretix.base.timeframes import (
from pretix.control.forms.widgets import Select2 from pretix.control.forms.widgets import Select2
from pretix.helpers.filenames import safe_for_filename from pretix.helpers.filenames import safe_for_filename
from pretix.helpers.iter import chunked_iterable from pretix.helpers.iter import chunked_iterable
from pretix.helpers.reportlab import FontFallbackParagraph from pretix.helpers.reportlab import PlainTextParagraph
from pretix.helpers.templatetags.jsonfield import JSONExtract from pretix.helpers.templatetags.jsonfield import JSONExtract
from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -344,7 +344,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
] ]
story = [ story = [
FontFallbackParagraph( PlainTextParagraph(
cl.name, cl.name,
headlinestyle headlinestyle
), ),
@@ -352,7 +352,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
if cl.subevent: if cl.subevent:
story += [ story += [
Spacer(1, 3 * mm), Spacer(1, 3 * mm),
FontFallbackParagraph( PlainTextParagraph(
'{} ({} {})'.format( '{} ({} {})'.format(
cl.subevent.name, cl.subevent.name,
cl.subevent.get_date_range_display(), cl.subevent.get_date_range_display(),
@@ -382,10 +382,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
headrowstyle.fontName = 'OpenSansBd' headrowstyle.fontName = 'OpenSansBd'
for q in questions: for q in questions:
txt = str(q.question) txt = str(q.question)
p = FontFallbackParagraph(txt, headrowstyle) p = PlainTextParagraph(txt, headrowstyle)
while p.wrap(colwidths[len(tdata[0])], 5000)[1] > 30 * mm: while p.wrap(colwidths[len(tdata[0])], 5000)[1] > 30 * mm:
txt = txt[:len(txt) - 50] + "..." txt = txt[:len(txt) - 50] + "..."
p = FontFallbackParagraph(txt, headrowstyle) p = PlainTextParagraph(txt, headrowstyle)
tdata[0].append(p) tdata[0].append(p)
qs = self._get_queryset(cl, form_data) qs = self._get_queryset(cl, form_data)
@@ -432,8 +432,8 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
CBFlowable(bool(op.last_checked_in)) if not op.blocked else '', CBFlowable(bool(op.last_checked_in)) if not op.blocked else '',
'' if op.order.status != Order.STATUS_PAID else '', '' if op.order.status != Order.STATUS_PAID else '',
op.order.code, op.order.code,
FontFallbackParagraph(name, self.get_style()), PlainTextParagraph(name, self.get_style()),
FontFallbackParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()), PlainTextParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()),
] ]
acache = {} acache = {}
if op.addon_to: if op.addon_to:
@@ -444,10 +444,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
for q in questions: for q in questions:
txt = acache.get(q.pk, '') txt = acache.get(q.pk, '')
txt = bleach.clean(txt, tags={'br'}).strip().replace('<br>', '<br/>') txt = bleach.clean(txt, tags={'br'}).strip().replace('<br>', '<br/>')
p = FontFallbackParagraph(txt, self.get_style()) p = PlainTextParagraph(txt, self.get_style())
while p.wrap(colwidths[len(row)], 5000)[1] > 50 * mm: while p.wrap(colwidths[len(row)], 5000)[1] > 50 * mm:
txt = txt[:len(txt) - 50] + "..." txt = txt[:len(txt) - 50] + "..."
p = FontFallbackParagraph(txt, self.get_style()) p = PlainTextParagraph(txt, self.get_style())
row.append(p) row.append(p)
if op.order.status != Order.STATUS_PAID: if op.order.status != Order.STATUS_PAID:
tstyledata += [ tstyledata += [
+78 -78
View File
@@ -36,7 +36,7 @@ from reportlab.lib import colors, pagesizes
from reportlab.lib.enums import TA_CENTER, TA_RIGHT from reportlab.lib.enums import TA_CENTER, TA_RIGHT
from reportlab.lib.units import mm from reportlab.lib.units import mm
from reportlab.platypus import ( from reportlab.platypus import (
KeepTogether, PageTemplate, Paragraph, Spacer, Table, TableStyle, KeepTogether, PageTemplate, Spacer, Table, TableStyle,
) )
from pretix.base.exporter import BaseExporter from pretix.base.exporter import BaseExporter
@@ -49,7 +49,7 @@ from pretix.base.timeframes import (
resolve_timeframe_to_datetime_start_inclusive_end_exclusive, resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
) )
from pretix.control.forms.filter import get_all_payment_providers from pretix.control.forms.filter import get_all_payment_providers
from pretix.helpers.reportlab import FontFallbackParagraph from pretix.helpers.reportlab import PlainTextParagraph
from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -311,13 +311,13 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata = [ tdata = [
[ [
FontFallbackParagraph(self._transaction_group_header_label(), tstyle_bold), PlainTextParagraph(self._transaction_group_header_label(), tstyle_bold),
FontFallbackParagraph(_("Price"), tstyle_bold_right), PlainTextParagraph(_("Price"), tstyle_bold_right),
FontFallbackParagraph(_("Tax rate"), tstyle_bold_right), PlainTextParagraph(_("Tax rate"), tstyle_bold_right),
FontFallbackParagraph("#", tstyle_bold_right), PlainTextParagraph("#", tstyle_bold_right),
FontFallbackParagraph(_("Net total"), tstyle_bold_right), PlainTextParagraph(_("Net total"), tstyle_bold_right),
FontFallbackParagraph(_("Tax total"), tstyle_bold_right), PlainTextParagraph(_("Tax total"), tstyle_bold_right),
FontFallbackParagraph(_("Gross total"), tstyle_bold_right), PlainTextParagraph(_("Gross total"), tstyle_bold_right),
] ]
] ]
@@ -347,12 +347,12 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if e != last_group: if e != last_group:
if last_group_head_idx > 0 and e is not None: if last_group_head_idx > 0 and e is not None:
tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right), tdata[last_group_head_idx][4] = PlainTextParagraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right), tdata[last_group_head_idx][5] = PlainTextParagraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right), tdata[last_group_head_idx][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
tdata.append( tdata.append(
[ [
FontFallbackParagraph( PlainTextParagraph(
e, e,
tstyle_bold, tstyle_bold,
), ),
@@ -375,20 +375,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
text = self._transaction_row_label(r) text = self._transaction_row_label(r)
tdata.append( tdata.append(
[ [
FontFallbackParagraph(text, tstyle), PlainTextParagraph(text, tstyle),
Paragraph( PlainTextParagraph(
money_filter(r["price"], currency) money_filter(r["price"], currency)
if "price" in r and r["price"] is not None if "price" in r and r["price"] is not None
else "", else "",
tstyle_right, tstyle_right,
), ),
Paragraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right), PlainTextParagraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
Paragraph(str(r["sum_cont"]), tstyle_right), PlainTextParagraph(str(r["sum_cont"]), tstyle_right),
Paragraph( PlainTextParagraph(
money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right
), ),
Paragraph(money_filter(r["sum_tax"], currency), tstyle_right), PlainTextParagraph(money_filter(r["sum_tax"], currency), tstyle_right),
Paragraph(money_filter(r["sum_price"], currency), tstyle_right), PlainTextParagraph(money_filter(r["sum_price"], currency), tstyle_right),
] ]
) )
sum_cnt_by_tax_rate[r["tax_rate"]] += r["sum_cont"] sum_cnt_by_tax_rate[r["tax_rate"]] += r["sum_cont"]
@@ -398,19 +398,19 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
sum_tax_by_group += r["sum_tax"] sum_tax_by_group += r["sum_tax"]
if last_group_head_idx > 0 and last_group is not None: if last_group_head_idx > 0 and last_group is not None:
tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right), tdata[last_group_head_idx][4] = PlainTextParagraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right), tdata[last_group_head_idx][5] = PlainTextParagraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right), tdata[last_group_head_idx][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
if len(sum_tax_by_tax_rate) > 1: if len(sum_tax_by_tax_rate) > 1:
for tax_rate in sorted(sum_tax_by_tax_rate.keys(), reverse=True): for tax_rate in sorted(sum_tax_by_tax_rate.keys(), reverse=True):
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Sum"), tstyle), PlainTextParagraph(_("Sum"), tstyle),
Paragraph("", tstyle_right), PlainTextParagraph("", tstyle_right),
Paragraph(localize(tax_rate.normalize()) + " %", tstyle_right), PlainTextParagraph(localize(tax_rate.normalize()) + " %", tstyle_right),
Paragraph("", tstyle_right), PlainTextParagraph("", tstyle_right),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum_price_by_tax_rate[tax_rate] sum_price_by_tax_rate[tax_rate]
- sum_tax_by_tax_rate[tax_rate], - sum_tax_by_tax_rate[tax_rate],
@@ -418,10 +418,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle_right, tstyle_right,
), ),
Paragraph( PlainTextParagraph(
money_filter(sum_tax_by_tax_rate[tax_rate], currency), tstyle_right money_filter(sum_tax_by_tax_rate[tax_rate], currency), tstyle_right
), ),
Paragraph( PlainTextParagraph(
money_filter(sum_price_by_tax_rate[tax_rate], currency), money_filter(sum_price_by_tax_rate[tax_rate], currency),
tstyle_right, tstyle_right,
), ),
@@ -439,11 +439,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Sum"), tstyle_bold), PlainTextParagraph(_("Sum"), tstyle_bold),
Paragraph("", tstyle_right), PlainTextParagraph("", tstyle_right),
Paragraph("", tstyle_right), PlainTextParagraph("", tstyle_right),
Paragraph("", tstyle_bold_right), PlainTextParagraph("", tstyle_bold_right),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum(sum_price_by_tax_rate.values()) sum(sum_price_by_tax_rate.values())
- sum(sum_tax_by_tax_rate.values()), - sum(sum_tax_by_tax_rate.values()),
@@ -451,11 +451,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle_bold_right, tstyle_bold_right,
), ),
Paragraph( PlainTextParagraph(
money_filter(sum(sum_tax_by_tax_rate.values()), currency), money_filter(sum(sum_tax_by_tax_rate.values()), currency),
tstyle_bold_right, tstyle_bold_right,
), ),
Paragraph( PlainTextParagraph(
money_filter(sum(sum_price_by_tax_rate.values()), currency), money_filter(sum(sum_price_by_tax_rate.values()), currency),
tstyle_bold_right, tstyle_bold_right,
), ),
@@ -493,10 +493,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata = [ tdata = [
[ [
FontFallbackParagraph(_("Payment method"), tstyle_bold), PlainTextParagraph(_("Payment method"), tstyle_bold),
FontFallbackParagraph(_("Payments"), tstyle_bold_right), PlainTextParagraph(_("Payments"), tstyle_bold_right),
FontFallbackParagraph(_("Refunds"), tstyle_bold_right), PlainTextParagraph(_("Refunds"), tstyle_bold_right),
FontFallbackParagraph(_("Total"), tstyle_bold_right), PlainTextParagraph(_("Total"), tstyle_bold_right),
] ]
] ]
@@ -537,20 +537,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
for p in providers: for p in providers:
tdata.append( tdata.append(
[ [
Paragraph(provider_names.get(p, p), tstyle), PlainTextParagraph(provider_names.get(p, p), tstyle),
FontFallbackParagraph( PlainTextParagraph(
money_filter(payments_by_provider[p], currency) money_filter(payments_by_provider[p], currency)
if p in payments_by_provider if p in payments_by_provider
else "", else "",
tstyle_right, tstyle_right,
), ),
Paragraph( PlainTextParagraph(
money_filter(refunds_by_provider[p], currency) money_filter(refunds_by_provider[p], currency)
if p in refunds_by_provider if p in refunds_by_provider
else "", else "",
tstyle_right, tstyle_right,
), ),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
payments_by_provider.get(p, Decimal("0.00")) payments_by_provider.get(p, Decimal("0.00"))
- refunds_by_provider.get(p, Decimal("0.00")), - refunds_by_provider.get(p, Decimal("0.00")),
@@ -563,20 +563,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Sum"), tstyle_bold), PlainTextParagraph(_("Sum"), tstyle_bold),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum(payments_by_provider.values(), Decimal("0.00")), currency sum(payments_by_provider.values(), Decimal("0.00")), currency
), ),
tstyle_bold_right, tstyle_bold_right,
), ),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum(refunds_by_provider.values(), Decimal("0.00")), currency sum(refunds_by_provider.values(), Decimal("0.00")), currency
), ),
tstyle_bold_right, tstyle_bold_right,
), ),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum(payments_by_provider.values(), Decimal("0.00")) sum(payments_by_provider.values(), Decimal("0.00"))
- sum(refunds_by_provider.values(), Decimal("0.00")), - sum(refunds_by_provider.values(), Decimal("0.00")),
@@ -641,7 +641,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
open_before = tx_before - p_before + r_before open_before = tx_before - p_before + r_before
tdata.append( tdata.append(
[ [
FontFallbackParagraph( PlainTextParagraph(
_("Pending payments at {datetime}").format( _("Pending payments at {datetime}").format(
datetime=date_format( datetime=date_format(
(df_start - datetime.timedelta.resolution).astimezone( (df_start - datetime.timedelta.resolution).astimezone(
@@ -653,7 +653,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tstyle, tstyle,
), ),
"", "",
Paragraph(money_filter(open_before, currency), tstyle_right), PlainTextParagraph(money_filter(open_before, currency), tstyle_right),
] ]
) )
else: else:
@@ -670,30 +670,30 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00") ] or Decimal("0.00")
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Orders"), tstyle), PlainTextParagraph(_("Orders"), tstyle),
Paragraph("+", tstyle_center), PlainTextParagraph("+", tstyle_center),
Paragraph(money_filter(tx_during, currency), tstyle_right), PlainTextParagraph(money_filter(tx_during, currency), tstyle_right),
] ]
) )
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Payments"), tstyle), PlainTextParagraph(_("Payments"), tstyle),
Paragraph("-", tstyle_center), PlainTextParagraph("-", tstyle_center),
Paragraph(money_filter(p_during, currency), tstyle_right), PlainTextParagraph(money_filter(p_during, currency), tstyle_right),
] ]
) )
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Refunds"), tstyle), PlainTextParagraph(_("Refunds"), tstyle),
Paragraph("+", tstyle_center), PlainTextParagraph("+", tstyle_center),
Paragraph(money_filter(r_during, currency), tstyle_right), PlainTextParagraph(money_filter(r_during, currency), tstyle_right),
] ]
) )
open_after = open_before + tx_during - p_during + r_during open_after = open_before + tx_during - p_during + r_during
tdata.append( tdata.append(
[ [
Paragraph( PlainTextParagraph(
_("Pending payments at {datetime}").format( _("Pending payments at {datetime}").format(
datetime=date_format( datetime=date_format(
((df_end or now()) - datetime.timedelta.resolution).astimezone( ((df_end or now()) - datetime.timedelta.resolution).astimezone(
@@ -704,8 +704,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle_bold, tstyle_bold,
), ),
Paragraph("=", tstyle_center), PlainTextParagraph("=", tstyle_center),
Paragraph(money_filter(open_after, currency), tstyle_bold_right), PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right),
] ]
) )
tstyledata += [ tstyledata += [
@@ -752,7 +752,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
) )
tdata.append( tdata.append(
[ [
Paragraph( PlainTextParagraph(
_("Total gift card value at {datetime}").format( _("Total gift card value at {datetime}").format(
datetime=date_format( datetime=date_format(
(df_start - datetime.timedelta.resolution).astimezone( (df_start - datetime.timedelta.resolution).astimezone(
@@ -763,7 +763,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle, tstyle,
), ),
Paragraph(money_filter(tx_before, currency), tstyle_right), PlainTextParagraph(money_filter(tx_before, currency), tstyle_right),
] ]
) )
else: else:
@@ -774,8 +774,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00") ] or Decimal("0.00")
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Gift card transactions (credit)"), tstyle), PlainTextParagraph(_("Gift card transactions (credit)"), tstyle),
Paragraph(money_filter(tx_during_pos, currency), tstyle_right), PlainTextParagraph(money_filter(tx_during_pos, currency), tstyle_right),
] ]
) )
@@ -784,15 +784,15 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00") ] or Decimal("0.00")
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Gift card transactions (debit)"), tstyle), PlainTextParagraph(_("Gift card transactions (debit)"), tstyle),
Paragraph(money_filter(tx_during_neg, currency), tstyle_right), PlainTextParagraph(money_filter(tx_during_neg, currency), tstyle_right),
] ]
) )
open_after = tx_before + tx_during_pos + tx_during_neg open_after = tx_before + tx_during_pos + tx_during_neg
tdata.append( tdata.append(
[ [
Paragraph( PlainTextParagraph(
_("Total gift card value at {datetime}").format( _("Total gift card value at {datetime}").format(
datetime=date_format( datetime=date_format(
((df_end or now()) - datetime.timedelta.resolution).astimezone( ((df_end or now()) - datetime.timedelta.resolution).astimezone(
@@ -803,7 +803,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle_bold, tstyle_bold,
), ),
Paragraph(money_filter(open_after, currency), tstyle_bold_right), PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right),
] ]
) )
tstyledata += [ tstyledata += [
@@ -854,10 +854,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
style_small.leading = 10 style_small.leading = 10
story = [ story = [
FontFallbackParagraph(self.verbose_name, style_h1), PlainTextParagraph(self.verbose_name, style_h1),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
FontFallbackParagraph( PlainTextParagraph(
"<br />".join(escape(f) for f in self.describe_filters(form_data)), "\n".join(escape(f) for f in self.describe_filters(form_data)),
style_small, style_small,
), ),
] ]
@@ -870,7 +870,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if s: if s:
story += [ story += [
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
FontFallbackParagraph(_("Orders") + c_head, style_h2), PlainTextParagraph(_("Orders") + c_head, style_h2),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
*s *s
] ]
@@ -881,7 +881,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if s: if s:
story += [ story += [
Spacer(0, 8 * mm), Spacer(0, 8 * mm),
FontFallbackParagraph(_("Payments") + c_head, style_h2), PlainTextParagraph(_("Payments") + c_head, style_h2),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
*s *s
] ]
@@ -894,7 +894,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
Spacer(0, 8 * mm), Spacer(0, 8 * mm),
KeepTogether( KeepTogether(
[ [
FontFallbackParagraph(_("Open items") + c_head, style_h2), PlainTextParagraph(_("Open items") + c_head, style_h2),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
*s *s
] ]
@@ -912,7 +912,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
Spacer(0, 8 * mm), Spacer(0, 8 * mm),
KeepTogether( KeepTogether(
[ [
FontFallbackParagraph(_("Gift cards") + c_head, style_h2), PlainTextParagraph(_("Gift cards") + c_head, style_h2),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
*s, *s,
] ]
+13 -13
View File
@@ -70,7 +70,7 @@ from pretix.base.timeframes import (
) )
from pretix.control.forms.filter import OverviewFilterForm from pretix.control.forms.filter import OverviewFilterForm
from pretix.helpers.reportlab import ( from pretix.helpers.reportlab import (
FontFallbackParagraph, register_ttf_font_if_new, PlainTextParagraph, register_ttf_font_if_new,
) )
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -282,7 +282,7 @@ class OverviewReport(Report):
headlinestyle.fontSize = 15 headlinestyle.fontSize = 15
headlinestyle.fontName = 'OpenSansBd' headlinestyle.fontName = 'OpenSansBd'
story = [ story = [
FontFallbackParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle), PlainTextParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle),
Spacer(1, 5 * mm) Spacer(1, 5 * mm)
] ]
return story return story
@@ -292,7 +292,7 @@ class OverviewReport(Report):
if form_data.get('date_axis') and form_data.get('date_range'): if form_data.get('date_axis') and form_data.get('date_range'):
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone) d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
story += [ story += [
FontFallbackParagraph(_('{axis} between {start} and {end}').format( PlainTextParagraph(_('{axis} between {start} and {end}').format(
axis=dict(OverviewFilterForm(event=self.event).fields['date_axis'].choices)[form_data.get('date_axis')], axis=dict(OverviewFilterForm(event=self.event).fields['date_axis'].choices)[form_data.get('date_axis')],
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '', start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '',
end=date_format(d_end, 'SHORT_DATE_FORMAT') if d_end else '', end=date_format(d_end, 'SHORT_DATE_FORMAT') if d_end else '',
@@ -305,13 +305,13 @@ class OverviewReport(Report):
subevent = self.event.subevents.get(pk=self.form_data.get('subevent')) subevent = self.event.subevents.get(pk=self.form_data.get('subevent'))
except SubEvent.DoesNotExist: except SubEvent.DoesNotExist:
subevent = self.form_data.get('subevent') subevent = self.form_data.get('subevent')
story.append(FontFallbackParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style())) story.append(PlainTextParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style()))
story.append(Spacer(1, 5 * mm)) story.append(Spacer(1, 5 * mm))
if form_data.get('subevent_date_range'): if form_data.get('subevent_date_range'):
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['subevent_date_range'], self.timezone) d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['subevent_date_range'], self.timezone)
story += [ story += [
FontFallbackParagraph(_('{axis} between {start} and {end}').format( PlainTextParagraph(_('{axis} between {start} and {end}').format(
axis=_('Event date'), axis=_('Event date'),
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '', start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '',
end=date_format(d_end - timedelta(hours=1), 'SHORT_DATE_FORMAT') if d_end else '', end=date_format(d_end - timedelta(hours=1), 'SHORT_DATE_FORMAT') if d_end else '',
@@ -384,13 +384,13 @@ class OverviewReport(Report):
tdata = [ tdata = [
[ [
_('Product'), _('Product'),
FontFallbackParagraph(_('Canceled'), tstyle_th), PlainTextParagraph(_('Canceled'), tstyle_th),
'', '',
FontFallbackParagraph(_('Expired'), tstyle_th), PlainTextParagraph(_('Expired'), tstyle_th),
'', '',
FontFallbackParagraph(_('Approval pending'), tstyle_th), PlainTextParagraph(_('Approval pending'), tstyle_th),
'', '',
FontFallbackParagraph(_('Purchased'), tstyle_th), PlainTextParagraph(_('Purchased'), tstyle_th),
'', '', '', '', '' '', '', '', '', ''
], ],
[ [
@@ -421,14 +421,14 @@ class OverviewReport(Report):
for tup in items_by_category: for tup in items_by_category:
if tup[0]: if tup[0]:
tdata.append([ tdata.append([
FontFallbackParagraph(str(tup[0]), tstyle_bold) PlainTextParagraph(str(tup[0]), tstyle_bold)
]) ])
for l, s in states: for l, s in states:
tdata[-1].append(str(tup[0].num[l][0])) tdata[-1].append(str(tup[0].num[l][0]))
tdata[-1].append(floatformat(tup[0].num[l][2 if net else 1], places)) tdata[-1].append(floatformat(tup[0].num[l][2 if net else 1], places))
for item in tup[1]: for item in tup[1]:
tdata.append([ tdata.append([
FontFallbackParagraph(str(item), tstyle) PlainTextParagraph(str(item), tstyle)
]) ])
for l, s in states: for l, s in states:
tdata[-1].append(str(item.num[l][0])) tdata[-1].append(str(item.num[l][0]))
@@ -436,7 +436,7 @@ class OverviewReport(Report):
if item.has_variations: if item.has_variations:
for var in item.all_variations: for var in item.all_variations:
tdata.append([ tdata.append([
FontFallbackParagraph(" " + str(var), tstyle) PlainTextParagraph(" " + str(var), tstyle)
]) ])
for l, s in states: for l, s in states:
tdata[-1].append(str(var.num[l][0])) tdata[-1].append(str(var.num[l][0]))
@@ -568,7 +568,7 @@ class OrderTaxListReportPDF(Report):
tstyledata.append(('SPAN', (5 + 2 * i, 0), (6 + 2 * i, 0))) tstyledata.append(('SPAN', (5 + 2 * i, 0), (6 + 2 * i, 0)))
story = [ story = [
FontFallbackParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle), PlainTextParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle),
Spacer(1, 5 * mm) Spacer(1, 5 * mm)
] ]
tdata = [ tdata = [