diff --git a/src/pretix/base/invoice.py b/src/pretix/base/invoice.py index b77c0125c..1a04269d6 100644 --- a/src/pretix/base/invoice.py +++ b/src/pretix/base/invoice.py @@ -20,6 +20,8 @@ # . # import logging +import re +import unicodedata from collections import defaultdict from decimal import Decimal from io import BytesIO @@ -28,6 +30,7 @@ from typing import Tuple import bleach import vat_moss.exchange_rates +from bidi.algorithm import get_display from django.contrib.staticfiles import finders from django.db.models import Sum from django.dispatch import receiver @@ -53,7 +56,8 @@ from pretix.base.models import Event, Invoice, Order, OrderPayment from pretix.base.services.currencies import SOURCE_NAMES from pretix.base.signals import register_invoice_renderers from pretix.base.templatetags.money import money_filter -from pretix.helpers.reportlab import ThumbnailingImageReader +from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper +from pretix.presale.style import get_fonts logger = logging.getLogger(__name__) @@ -79,7 +83,12 @@ class NumberedCanvas(Canvas): def draw_page_number(self, page_count): self.saveState() self.setFont(self.font_regular, 8) - self.drawRightString(self._pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d of %d") % (self._pageNumber, page_count,)) + text = pgettext("invoice", "Page %d of %d") % (self._pageNumber, page_count,) + try: + text = get_display(reshaper.reshape(text)) + except: + logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text))) + self.drawRightString(self._pagesize[0] - 20 * mm, 10 * mm, text) self.restoreState() @@ -139,8 +148,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer): """ Initialize the renderer. By default, this registers fonts and sets ``self.stylesheet``. """ - self.stylesheet = self._get_stylesheet() self._register_fonts() + self.stylesheet = self._get_stylesheet() def _get_stylesheet(self): """ @@ -148,6 +157,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer): """ stylesheet = StyleSheet1() stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12)) + stylesheet.add(ParagraphStyle(name='Bold', fontName=self.font_bold, fontSize=10, leading=12)) + stylesheet.add(ParagraphStyle(name='BoldRight', fontName=self.font_bold, fontSize=10, leading=12, alignment=TA_RIGHT)) + stylesheet.add(ParagraphStyle(name='BoldRightNoSplit', fontName=self.font_bold, fontSize=10, leading=12, alignment=TA_RIGHT, + splitLongWords=False)) stylesheet.add(ParagraphStyle(name='NormalRight', fontName=self.font_regular, fontSize=10, leading=12, alignment=TA_RIGHT)) stylesheet.add(ParagraphStyle(name='BoldInverseCenter', fontName=self.font_bold, fontSize=10, leading=12, textColor=colors.white, alignment=TA_CENTER)) @@ -155,6 +168,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer): 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)) stylesheet.add(ParagraphStyle(name='Fineprint', fontName=self.font_regular, fontSize=8, leading=10)) + stylesheet.add(ParagraphStyle(name='FineprintRight', fontName=self.font_regular, fontSize=8, leading=10, alignment=TA_RIGHT)) return stylesheet def _register_fonts(self): @@ -168,6 +182,32 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer): pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd', italic='OpenSansIt', boldItalic='OpenSansBI') + for family, styles in get_fonts().items(): + if family == self.event.settings.invoice_renderer_font: + pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype']))) + self.font_regular = family + if 'italic' in styles: + pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype']))) + if 'bold' in styles: + pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype']))) + self.font_bold = family + ' B' + if 'bolditalic' in styles: + pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype']))) + + def _normalize(self, text): + # 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 = "
".join(get_display(reshaper.reshape(l)) for l in re.split("
", text)) + except: + logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text))) + + return text + def _upper(self, val): # We uppercase labels, but not in every language if get_language().startswith('el'): @@ -247,10 +287,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer): return 'invoice.pdf', 'application/pdf', buffer.read() def _clean_text(self, text, tags=None): - return bleach.clean( + return self._normalize(bleach.clean( text, tags=tags or [] - ).strip().replace('
', '
').replace('\n', '
\n') + ).strip().replace('
', '
').replace('\n', '
\n')) class PaidMarker(Flowable): @@ -291,7 +331,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): canvas.setFont(self.font_regular, 8) for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]): - canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip()) + canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, self._normalize(line.strip())) canvas.restoreState() @@ -324,13 +364,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): def _draw_invoice_from_label(self, canvas): textobject = canvas.beginText(25 * mm, (297 - 15) * mm) textobject.setFont(self.font_bold, 8) - textobject.textLine(self._upper(pgettext('invoice', 'Invoice from'))) + textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice from')))) canvas.drawText(textobject) def _draw_invoice_to_label(self, canvas): textobject = canvas.beginText(25 * mm, (297 - 50) * mm) textobject.setFont(self.font_bold, 8) - textobject.textLine(self._upper(pgettext('invoice', 'Invoice to'))) + textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice to')))) canvas.drawText(textobject) logo_width = 25 * mm @@ -358,51 +398,51 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): def _draw_metadata(self, canvas): textobject = canvas.beginText(125 * mm, (297 - 38) * mm) textobject.setFont(self.font_bold, 8) - textobject.textLine(self._upper(pgettext('invoice', 'Order code'))) + textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Order code')))) textobject.moveCursor(0, 5) textobject.setFont(self.font_regular, 10) - textobject.textLine(self.invoice.order.full_code) + textobject.textLine(self._normalize(self.invoice.order.full_code)) canvas.drawText(textobject) textobject = canvas.beginText(125 * mm, (297 - 50) * mm) textobject.setFont(self.font_bold, 8) if self.invoice.is_cancellation: - textobject.textLine(self._upper(pgettext('invoice', 'Cancellation number'))) + textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Cancellation number')))) textobject.moveCursor(0, 5) textobject.setFont(self.font_regular, 10) - textobject.textLine(self.invoice.number) + textobject.textLine(self._normalize(self.invoice.number)) textobject.moveCursor(0, 5) textobject.setFont(self.font_bold, 8) - textobject.textLine(self._upper(pgettext('invoice', 'Original invoice'))) + textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Original invoice')))) textobject.moveCursor(0, 5) textobject.setFont(self.font_regular, 10) - textobject.textLine(self.invoice.refers.number) + textobject.textLine(self._normalize(self.invoice.refers.number)) else: - textobject.textLine(self._upper(pgettext('invoice', 'Invoice number'))) + textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice number')))) textobject.moveCursor(0, 5) textobject.setFont(self.font_regular, 10) - textobject.textLine(self.invoice.number) + textobject.textLine(self._normalize(self.invoice.number)) textobject.moveCursor(0, 5) if self.invoice.is_cancellation: textobject.setFont(self.font_bold, 8) - textobject.textLine(self._upper(pgettext('invoice', 'Cancellation date'))) + textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Cancellation date')))) textobject.moveCursor(0, 5) textobject.setFont(self.font_regular, 10) - textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT")) + textobject.textLine(self._normalize(date_format(self.invoice.date, "DATE_FORMAT"))) textobject.moveCursor(0, 5) textobject.setFont(self.font_bold, 8) - textobject.textLine(self._upper(pgettext('invoice', 'Original invoice date'))) + textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Original invoice date')))) textobject.moveCursor(0, 5) textobject.setFont(self.font_regular, 10) - textobject.textLine(date_format(self.invoice.refers.date, "DATE_FORMAT")) + textobject.textLine(self._normalize(date_format(self.invoice.refers.date, "DATE_FORMAT"))) textobject.moveCursor(0, 5) else: textobject.setFont(self.font_bold, 8) - textobject.textLine(self._upper(pgettext('invoice', 'Invoice date'))) + textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice date')))) textobject.moveCursor(0, 5) textobject.setFont(self.font_regular, 10) - textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT")) + textobject.textLine(self._normalize(date_format(self.invoice.date, "DATE_FORMAT"))) textobject.moveCursor(0, 5) canvas.drawText(textobject) @@ -415,19 +455,19 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): def _draw_event_label(self, canvas): textobject = canvas.beginText(125 * mm, (297 - 15) * mm) textobject.setFont(self.font_bold, 8) - textobject.textLine(self._upper(pgettext('invoice', 'Event'))) + textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Event')))) canvas.drawText(textobject) def _draw_event(self, canvas): def shorten(txt): txt = str(txt) txt = bleach.clean(txt, tags=[]).strip() - p = Paragraph(txt.strip().replace('\n', '
\n'), style=self.stylesheet['Normal']) + p = Paragraph(self._normalize(txt.strip().replace('\n', '
\n')), style=self.stylesheet['Normal']) p_size = p.wrap(self.event_width, self.event_height) while p_size[1] > 2 * self.stylesheet['Normal'].leading: txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…' - p = Paragraph(txt.strip().replace('\n', '
\n'), style=self.stylesheet['Normal']) + p = Paragraph(self._normalize(txt.strip().replace('\n', '
\n')), style=self.stylesheet['Normal']) p_size = p.wrap(self.event_width, self.event_height) return txt @@ -453,7 +493,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): else: p_str = shorten(self.invoice.event.name) - p = Paragraph(p_str.strip().replace('\n', '
\n'), style=self.stylesheet['Normal']) + p = Paragraph(self._normalize(p_str.strip().replace('\n', '
\n')), style=self.stylesheet['Normal']) p.wrapOn(canvas, 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]) @@ -462,12 +502,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): def _draw_footer(self, canvas): canvas.setFont(self.font_regular, 8) for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]): - canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip()) + canvas.drawCentredString(self.pagesize[0] / 2, 25 + (3.5 * i) * mm, self._normalize(line.strip())) def _draw_testmode(self, canvas): if self.invoice.order.testmode: canvas.saveState() - canvas.setFont('OpenSansBd', 30) + canvas.setFont(self.font_bold, 30) canvas.setFillColorRGB(32, 0, 0) canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE')) canvas.restoreState() @@ -552,10 +592,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): story = [ NextPageTemplate('FirstPage'), Paragraph( - ( + self._normalize( pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU' else pgettext('invoice', 'Invoice') - ) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'), + ) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')), self.stylesheet['Heading1'] ), Spacer(1, 5 * mm), @@ -577,17 +617,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): ] if has_taxes: tdata = [( - pgettext('invoice', 'Description'), - pgettext('invoice', 'Qty'), - pgettext('invoice', 'Tax rate'), - pgettext('invoice', 'Net'), - pgettext('invoice', 'Gross'), + Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']), + Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']), + Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']), + Paragraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']), + Paragraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']), )] else: tdata = [( - pgettext('invoice', 'Description'), - pgettext('invoice', 'Qty'), - pgettext('invoice', 'Amount'), + Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['BoldRight']), + Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']), + Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']), )] def _group_key(line): @@ -634,13 +674,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): if has_taxes: tdata.append([ - pgettext('invoice', 'Invoice total'), '', '', '', + Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '', money_filter(total, self.invoice.event.currency) ]) colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)] else: tdata.append([ - pgettext('invoice', 'Invoice total'), '', + Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', money_filter(total, self.invoice.event.currency) ]) colwidths = [a * doc.width for a in (.65, .20, .15)] @@ -649,12 +689,16 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): 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 ['']) + [ - money_filter(pending_sum - total, self.invoice.event.currency) - ]) - tdata.append([pgettext('invoice', 'Outstanding payments')] + (['', '', ''] if has_taxes else ['']) + [ - money_filter(pending_sum, self.invoice.event.currency) - ]) + tdata.append( + [Paragraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] + + (['', '', ''] if has_taxes else ['']) + + [money_filter(pending_sum - total, self.invoice.event.currency)] + ) + tdata.append( + [Paragraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] + + (['', '', ''] if has_taxes else ['']) + + [money_filter(pending_sum, self.invoice.event.currency)] + ) tstyledata += [ ('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold), ] @@ -667,19 +711,24 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): ).aggregate( s=Sum('amount') )['s'] or Decimal('0.00') - tdata.append([pgettext('invoice', 'Paid by gift card')] + (['', '', ''] if has_taxes else ['']) + [ - money_filter(giftcard_sum, self.invoice.event.currency) - ]) - tdata.append([pgettext('invoice', 'Remaining amount')] + (['', '', ''] if has_taxes else ['']) + [ - money_filter(total - giftcard_sum, self.invoice.event.currency) - ]) + tdata.append( + [Paragraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] + + (['', '', ''] if has_taxes else ['']) + + [money_filter(giftcard_sum, self.invoice.event.currency)] + ) + tdata.append( + [Paragraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] + + (['', '', ''] if has_taxes else ['']) + + [money_filter(total - giftcard_sum, self.invoice.event.currency)] + ) 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, + text=self._normalize(self.invoice.payment_provider_stamp), color=colors.HexColor(self.event.settings.theme_color_success), + font=self.font_bold, size=16 ) tdata[-1][-2] = pm @@ -692,7 +741,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): if self.invoice.payment_provider_text: story.append(Paragraph( - self.invoice.payment_provider_text, + self._normalize(self.invoice.payment_provider_text), self.stylesheet['Normal'] )) @@ -716,10 +765,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): ('FONTNAME', (0, 0), (-1, -1), self.font_regular), ] thead = [ - pgettext('invoice', 'Tax rate'), - pgettext('invoice', 'Net value'), - pgettext('invoice', 'Gross value'), - pgettext('invoice', 'Tax'), + Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']), + Paragraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']), + Paragraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']), + Paragraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']), '' ] tdata = [thead] @@ -730,7 +779,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): continue tax = taxvalue_map[idx] tdata.append([ - localize(rate) + " % " + name, + Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']), money_filter(gross - tax, self.invoice.event.currency), money_filter(gross, self.invoice.event.currency), money_filter(tax, self.invoice.event.currency), @@ -749,7 +798,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): table.setStyle(TableStyle(tstyledata)) story.append(Spacer(5 * mm, 5 * mm)) story.append(KeepTogether([ - Paragraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']), + Paragraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']), table ])) @@ -776,12 +825,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): story.append(KeepTogether([ Spacer(1, height=2 * mm), Paragraph( - pgettext( + self._normalize(pgettext( 'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on ' '{date}, this corresponds to:' ).format(rate=localize(self.invoice.foreign_currency_rate), 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'] ), Spacer(1, height=3 * mm), @@ -790,16 +839,16 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate: foreign_total = round_decimal(total * self.invoice.foreign_currency_rate) story.append(Spacer(1, 5 * mm)) - story.append(Paragraph( + story.append(self._normalize(Paragraph( pgettext( 'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on ' '{date}, the invoice total corresponds to {total}.' ).format(rate=localize(self.invoice.foreign_currency_rate), date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"), authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"), - total=fmt(foreign_total)), + total=fmt(foreign_total)))), self.stylesheet['Fineprint'] - )) + ) return story @@ -843,7 +892,7 @@ class Modern1Renderer(ClassicInvoiceRenderer): self._clean_text(l) for l in self.invoice.address_invoice_from.strip().split('\n') ] - p = Paragraph(' · '.join(c), style=self.stylesheet['Sender']) + p = Paragraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender']) 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) super()._draw_invoice_from(canvas) @@ -880,15 +929,15 @@ class Modern1Renderer(ClassicInvoiceRenderer): return False textobject = canvas.beginText(x, self.pagesize[1] - begin_top) textobject.setFont(self.font_regular, 8) - textobject.textLine(label) + textobject.textLine(self._normalize(label)) textobject.moveCursor(0, 5) textobject.setFont(self.font_bold if bold else self.font_regular, value_size) - textobject.textLine(value) + textobject.textLine(self._normalize(value)) if sublabel: textobject.moveCursor(0, 1) textobject.setFont(self.font_regular, 8) - textobject.textLine(sublabel) + textobject.textLine(self._normalize(sublabel)) return textobject @@ -903,7 +952,7 @@ class Modern1Renderer(ClassicInvoiceRenderer): ] p = Paragraph( - date_format(self.invoice.date, "DATE_FORMAT"), + self._normalize(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) ) w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize) @@ -934,9 +983,9 @@ class Modern1Renderer(ClassicInvoiceRenderer): textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top) textobject.setFont(self.font_regular, 8) if self.invoice.is_cancellation: - textobject.textLine(pgettext('invoice', 'Cancellation date')) + textobject.textLine(self._normalize(pgettext('invoice', 'Cancellation date'))) else: - textobject.textLine(pgettext('invoice', 'Invoice date')) + textobject.textLine(self._normalize(pgettext('invoice', 'Invoice date'))) canvas.drawText(textobject) diff --git a/src/pretix/base/pdf.py b/src/pretix/base/pdf.py index c243c6d05..6f38d2ab1 100644 --- a/src/pretix/base/pdf.py +++ b/src/pretix/base/pdf.py @@ -48,7 +48,6 @@ from functools import partial from io import BytesIO import jsonschema -from arabic_reshaper import ArabicReshaper from bidi.algorithm import get_display from django.conf import settings from django.contrib.staticfiles import finders @@ -57,7 +56,6 @@ from django.db.models import Max, Min from django.dispatch import receiver from django.utils.deconstruct import deconstructible from django.utils.formats import date_format -from django.utils.functional import SimpleLazyObject from django.utils.html import conditional_escape from django.utils.timezone import now from django.utils.translation import gettext_lazy as _, pgettext @@ -78,12 +76,12 @@ from reportlab.pdfgen.canvas import Canvas from reportlab.platypus import Paragraph from pretix.base.i18n import language -from pretix.base.invoice import ThumbnailingImageReader from pretix.base.models import Order, OrderPosition, Question from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.signals import layout_image_variables, layout_text_variables from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.phone_format import phone_format +from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper from pretix.presale.style import get_fonts logger = logging.getLogger(__name__) @@ -699,12 +697,6 @@ def get_seat(op: OrderPosition): return None -reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={ - 'delete_harakat': True, - 'support_ligatures': False, -})) - - class Renderer: def __init__(self, event, layout, background_file): diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 9f9fc38a7..29e641c00 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -96,6 +96,18 @@ def primary_font_kwargs(): } +def invoice_font_kwargs(): + from pretix.presale.style import get_fonts + + choices = [('Open Sans', 'Open Sans')] + choices += sorted([ + (a, a) for a, v in get_fonts().items() + ], key=lambda a: a[0]) + return { + 'choices': choices, + } + + def restricted_plugin_kwargs(): from pretix.base.plugins import get_all_plugins @@ -644,6 +656,19 @@ DEFAULTS = { help_text=_("Only respected by some invoice renderers."), ) }, + 'invoice_renderer_font': { + 'default': 'Open Sans', + 'type': str, + 'form_class': forms.ChoiceField, + 'serializer_class': serializers.ChoiceField, + 'serializer_kwargs': lambda: dict(**invoice_font_kwargs()), + 'form_kwargs': lambda: dict( + label=_('Font'), + help_text=_("Only respected by some invoice renderers."), + required=True, + **invoice_font_kwargs() + ), + }, 'invoice_renderer': { 'default': 'classic', # default for new events is 'modern1' 'type': str, diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 85c3883ab..075ead570 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -857,6 +857,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm): 'invoice_eu_currencies', 'invoice_logo_image', 'invoice_renderer_highlight_order_code', + 'invoice_renderer_font', ] invoice_generate_sales_channels = forms.MultipleChoiceField( diff --git a/src/pretix/control/templates/pretixcontrol/event/invoicing.html b/src/pretix/control/templates/pretixcontrol/event/invoicing.html index d3fc957f8..6c7c47020 100644 --- a/src/pretix/control/templates/pretixcontrol/event/invoicing.html +++ b/src/pretix/control/templates/pretixcontrol/event/invoicing.html @@ -54,6 +54,7 @@ {% bootstrap_field form.invoice_additional_text layout="control" %} {% bootstrap_field form.invoice_footer_text layout="control" %} {% bootstrap_field form.invoice_logo_image layout="control" %} + {% bootstrap_field form.invoice_renderer_font layout="control" %} {% bootstrap_field form.invoice_renderer_highlight_order_code layout="control" %} {% bootstrap_field form.invoice_eu_currencies layout="control" %} diff --git a/src/pretix/helpers/reportlab.py b/src/pretix/helpers/reportlab.py index c38a6de89..58f92d6b9 100644 --- a/src/pretix/helpers/reportlab.py +++ b/src/pretix/helpers/reportlab.py @@ -19,6 +19,8 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +from arabic_reshaper import ArabicReshaper +from django.utils.functional import SimpleLazyObject from PIL.Image import Resampling from reportlab.lib.utils import ImageReader @@ -41,3 +43,9 @@ class ThumbnailingImageReader(ImageReader): # file handle if the file is a JPEG, and therefore does not respect the # (smaller) size of the modified image. return None + + +reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={ + 'delete_harakat': True, + 'support_ligatures': False, +}))