Invoices: Support font choice and Arabic text (#3343)

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2023-05-23 11:35:56 +02:00
committed by GitHub
parent f7d52abb0e
commit 364d86085c
6 changed files with 162 additions and 86 deletions

View File

@@ -20,6 +20,8 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import logging import logging
import re
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
@@ -28,6 +30,7 @@ from typing import Tuple
import bleach import bleach
import vat_moss.exchange_rates import vat_moss.exchange_rates
from bidi.algorithm import get_display
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
from django.db.models import Sum from django.db.models import Sum
from django.dispatch import receiver 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.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 ThumbnailingImageReader from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -79,7 +83,12 @@ class NumberedCanvas(Canvas):
def draw_page_number(self, page_count): def draw_page_number(self, page_count):
self.saveState() self.saveState()
self.setFont(self.font_regular, 8) 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() self.restoreState()
@@ -139,8 +148,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
""" """
Initialize the renderer. By default, this registers fonts and sets ``self.stylesheet``. Initialize the renderer. By default, this registers fonts and sets ``self.stylesheet``.
""" """
self.stylesheet = self._get_stylesheet()
self._register_fonts() self._register_fonts()
self.stylesheet = self._get_stylesheet()
def _get_stylesheet(self): def _get_stylesheet(self):
""" """
@@ -148,6 +157,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
""" """
stylesheet = StyleSheet1() stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12)) 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='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, stylesheet.add(ParagraphStyle(name='BoldInverseCenter', fontName=self.font_bold, fontSize=10, leading=12,
textColor=colors.white, alignment=TA_CENTER)) 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='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='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='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 return stylesheet
def _register_fonts(self): def _register_fonts(self):
@@ -168,6 +182,32 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd', pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
italic='OpenSansIt', boldItalic='OpenSansBI') 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 = "<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
if get_language().startswith('el'): if get_language().startswith('el'):
@@ -247,10 +287,10 @@ 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 bleach.clean( return self._normalize(bleach.clean(
text, text,
tags=tags or [] tags=tags or []
).strip().replace('<br>', '<br />').replace('\n', '<br />\n') ).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
class PaidMarker(Flowable): class PaidMarker(Flowable):
@@ -291,7 +331,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
canvas.setFont(self.font_regular, 8) canvas.setFont(self.font_regular, 8)
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]): 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() canvas.restoreState()
@@ -324,13 +364,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_invoice_from_label(self, canvas): def _draw_invoice_from_label(self, canvas):
textobject = canvas.beginText(25 * mm, (297 - 15) * mm) textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8) 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) canvas.drawText(textobject)
def _draw_invoice_to_label(self, canvas): def _draw_invoice_to_label(self, canvas):
textobject = canvas.beginText(25 * mm, (297 - 50) * mm) textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
textobject.setFont(self.font_bold, 8) 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) canvas.drawText(textobject)
logo_width = 25 * mm logo_width = 25 * mm
@@ -358,51 +398,51 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_metadata(self, canvas): def _draw_metadata(self, canvas):
textobject = canvas.beginText(125 * mm, (297 - 38) * mm) textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont(self.font_bold, 8) 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.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10) 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) canvas.drawText(textobject)
textobject = canvas.beginText(125 * mm, (297 - 50) * mm) textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
textobject.setFont(self.font_bold, 8) textobject.setFont(self.font_bold, 8)
if self.invoice.is_cancellation: 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.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10) textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number) textobject.textLine(self._normalize(self.invoice.number))
textobject.moveCursor(0, 5) textobject.moveCursor(0, 5)
textobject.setFont(self.font_bold, 8) 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.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10) textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.refers.number) textobject.textLine(self._normalize(self.invoice.refers.number))
else: else:
textobject.textLine(self._upper(pgettext('invoice', 'Invoice number'))) textobject.textLine(self._normalize(self._upper(pgettext('invoice', 'Invoice number'))))
textobject.moveCursor(0, 5) textobject.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10) textobject.setFont(self.font_regular, 10)
textobject.textLine(self.invoice.number) textobject.textLine(self._normalize(self.invoice.number))
textobject.moveCursor(0, 5) textobject.moveCursor(0, 5)
if self.invoice.is_cancellation: if self.invoice.is_cancellation:
textobject.setFont(self.font_bold, 8) 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.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10) 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.moveCursor(0, 5)
textobject.setFont(self.font_bold, 8) 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.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10) 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) textobject.moveCursor(0, 5)
else: else:
textobject.setFont(self.font_bold, 8) 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.moveCursor(0, 5)
textobject.setFont(self.font_regular, 10) 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.moveCursor(0, 5)
canvas.drawText(textobject) canvas.drawText(textobject)
@@ -415,19 +455,19 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event_label(self, canvas): def _draw_event_label(self, canvas):
textobject = canvas.beginText(125 * mm, (297 - 15) * mm) textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont(self.font_bold, 8) 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) canvas.drawText(textobject)
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=[]).strip() txt = bleach.clean(txt, tags=[]).strip()
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal']) p = Paragraph(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 = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal']) p = Paragraph(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)
return txt return txt
@@ -453,7 +493,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
else: else:
p_str = shorten(self.invoice.event.name) p_str = shorten(self.invoice.event.name)
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal']) p = Paragraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), 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])
@@ -462,14 +502,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_footer(self, canvas): def _draw_footer(self, canvas):
canvas.setFont(self.font_regular, 8) canvas.setFont(self.font_regular, 8)
for i, line in enumerate(self.invoice.footer_text.split('\n')[::-1]): 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): def _draw_testmode(self, canvas):
if self.invoice.order.testmode: if self.invoice.order.testmode:
canvas.saveState() canvas.saveState()
canvas.setFont('OpenSansBd', 30) canvas.setFont(self.font_bold, 30)
canvas.setFillColorRGB(32, 0, 0) canvas.setFillColorRGB(32, 0, 0)
canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, gettext('TEST MODE')) canvas.drawRightString(self.pagesize[0] - 20 * mm, (297 - 100) * mm, self._normalize(gettext('TEST MODE')))
canvas.restoreState() canvas.restoreState()
def _on_first_page(self, canvas: Canvas, doc): def _on_first_page(self, canvas: Canvas, doc):
@@ -517,22 +557,22 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.internal_reference: if self.invoice.internal_reference:
story.append(Paragraph( story.append(Paragraph(
pgettext('invoice', 'Customer reference: {reference}').format( self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
reference=self._clean_text(self.invoice.internal_reference), reference=self._clean_text(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(Paragraph( story.append(Paragraph(
pgettext('invoice', 'Customer VAT ID') + ': ' + self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
self._clean_text(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(Paragraph( story.append(Paragraph(
pgettext('invoice', 'Beneficiary') + ':<br />' + self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
self._clean_text(self.invoice.invoice_to_beneficiary), self._clean_text(self.invoice.invoice_to_beneficiary),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
@@ -552,10 +592,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [ story = [
NextPageTemplate('FirstPage'), NextPageTemplate('FirstPage'),
Paragraph( Paragraph(
( 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 pgettext('invoice', 'Cancellation'), ) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')),
self.stylesheet['Heading1'] self.stylesheet['Heading1']
), ),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
@@ -577,17 +617,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
] ]
if has_taxes: if has_taxes:
tdata = [( tdata = [(
pgettext('invoice', 'Description'), Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
pgettext('invoice', 'Qty'), Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
pgettext('invoice', 'Tax rate'), Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
pgettext('invoice', 'Net'), Paragraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
pgettext('invoice', 'Gross'), Paragraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
)] )]
else: else:
tdata = [( tdata = [(
pgettext('invoice', 'Description'), Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['BoldRight']),
pgettext('invoice', 'Qty'), Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
pgettext('invoice', 'Amount'), Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
)] )]
def _group_key(line): def _group_key(line):
@@ -634,13 +674,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes: if has_taxes:
tdata.append([ tdata.append([
pgettext('invoice', 'Invoice total'), '', '', '', Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency) money_filter(total, self.invoice.event.currency)
]) ])
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)] colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
else: else:
tdata.append([ tdata.append([
pgettext('invoice', 'Invoice total'), '', Paragraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency) money_filter(total, self.invoice.event.currency)
]) ])
colwidths = [a * doc.width for a in (.65, .20, .15)] 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: if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING:
pending_sum = self.invoice.order.pending_sum pending_sum = self.invoice.order.pending_sum
if pending_sum != total: if pending_sum != total:
tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [ tdata.append(
money_filter(pending_sum - total, self.invoice.event.currency) [Paragraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
]) (['', '', ''] if has_taxes else ['']) +
tdata.append([pgettext('invoice', 'Outstanding payments')] + (['', '', ''] if has_taxes else ['']) + [ [money_filter(pending_sum - total, self.invoice.event.currency)]
money_filter(pending_sum, 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 += [ tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold), ('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
] ]
@@ -667,19 +711,24 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
).aggregate( ).aggregate(
s=Sum('amount') s=Sum('amount')
)['s'] or Decimal('0.00') )['s'] or Decimal('0.00')
tdata.append([pgettext('invoice', 'Paid by gift card')] + (['', '', ''] if has_taxes else ['']) + [ tdata.append(
money_filter(giftcard_sum, self.invoice.event.currency) [Paragraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
]) (['', '', ''] if has_taxes else ['']) +
tdata.append([pgettext('invoice', 'Remaining amount')] + (['', '', ''] if has_taxes else ['']) + [ [money_filter(giftcard_sum, self.invoice.event.currency)]
money_filter(total - 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 += [ tstyledata += [
('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold), ('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold),
] ]
elif self.invoice.payment_provider_stamp: elif self.invoice.payment_provider_stamp:
pm = PaidMarker( 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), color=colors.HexColor(self.event.settings.theme_color_success),
font=self.font_bold,
size=16 size=16
) )
tdata[-1][-2] = pm tdata[-1][-2] = pm
@@ -692,7 +741,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if self.invoice.payment_provider_text: if self.invoice.payment_provider_text:
story.append(Paragraph( story.append(Paragraph(
self.invoice.payment_provider_text, self._normalize(self.invoice.payment_provider_text),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
@@ -716,10 +765,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('FONTNAME', (0, 0), (-1, -1), self.font_regular), ('FONTNAME', (0, 0), (-1, -1), self.font_regular),
] ]
thead = [ thead = [
pgettext('invoice', 'Tax rate'), Paragraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
pgettext('invoice', 'Net value'), Paragraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
pgettext('invoice', 'Gross value'), Paragraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
pgettext('invoice', 'Tax'), Paragraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
'' ''
] ]
tdata = [thead] tdata = [thead]
@@ -730,7 +779,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
continue continue
tax = taxvalue_map[idx] tax = taxvalue_map[idx]
tdata.append([ 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 - 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),
@@ -749,7 +798,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([
Paragraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']), Paragraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
table table
])) ]))
@@ -766,7 +815,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net = gross - tax net = gross - tax
tdata.append([ tdata.append([
localize(rate) + " % " + name, Paragraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
fmt(net), fmt(gross), fmt(tax), '' fmt(net), fmt(gross), fmt(tax), ''
]) ])
@@ -776,12 +825,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(KeepTogether([ story.append(KeepTogether([
Spacer(1, height=2 * mm), Spacer(1, height=2 * mm),
Paragraph( Paragraph(
pgettext( self._normalize(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),
@@ -790,14 +839,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(Paragraph( story.append(Paragraph(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}, 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']
)) ))
@@ -843,7 +892,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
self._clean_text(l) self._clean_text(l)
for l in self.invoice.address_invoice_from.strip().split('\n') 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.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)
@@ -880,15 +929,15 @@ class Modern1Renderer(ClassicInvoiceRenderer):
return False return False
textobject = canvas.beginText(x, self.pagesize[1] - begin_top) textobject = canvas.beginText(x, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8) textobject.setFont(self.font_regular, 8)
textobject.textLine(label) textobject.textLine(self._normalize(label))
textobject.moveCursor(0, 5) textobject.moveCursor(0, 5)
textobject.setFont(self.font_bold if bold else self.font_regular, value_size) textobject.setFont(self.font_bold if bold else self.font_regular, value_size)
textobject.textLine(value) textobject.textLine(self._normalize(value))
if sublabel: if sublabel:
textobject.moveCursor(0, 1) textobject.moveCursor(0, 1)
textobject.setFont(self.font_regular, 8) textobject.setFont(self.font_regular, 8)
textobject.textLine(sublabel) textobject.textLine(self._normalize(sublabel))
return textobject return textobject
@@ -903,7 +952,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
] ]
p = Paragraph( 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) 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)
@@ -934,9 +983,9 @@ class Modern1Renderer(ClassicInvoiceRenderer):
textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top) textobject = canvas.beginText(date_x, self.pagesize[1] - begin_top)
textobject.setFont(self.font_regular, 8) textobject.setFont(self.font_regular, 8)
if self.invoice.is_cancellation: if self.invoice.is_cancellation:
textobject.textLine(pgettext('invoice', 'Cancellation date')) textobject.textLine(self._normalize(pgettext('invoice', 'Cancellation date')))
else: else:
textobject.textLine(pgettext('invoice', 'Invoice date')) textobject.textLine(self._normalize(pgettext('invoice', 'Invoice date')))
canvas.drawText(textobject) canvas.drawText(textobject)

View File

@@ -48,7 +48,6 @@ from functools import partial
from io import BytesIO from io import BytesIO
import jsonschema import jsonschema
from arabic_reshaper import ArabicReshaper
from bidi.algorithm import get_display from bidi.algorithm import get_display
from django.conf import settings from django.conf import settings
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
@@ -57,7 +56,6 @@ from django.db.models import Max, Min
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.deconstruct import deconstructible from django.utils.deconstruct import deconstructible
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.functional import SimpleLazyObject
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext 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 reportlab.platypus import Paragraph
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.invoice import ThumbnailingImageReader
from pretix.base.models import Order, OrderPosition, Question from pretix.base.models import Order, OrderPosition, Question
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.phone_format import phone_format from pretix.base.templatetags.phone_format import phone_format
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -699,12 +697,6 @@ def get_seat(op: OrderPosition):
return None return None
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
'delete_harakat': True,
'support_ligatures': False,
}))
class Renderer: class Renderer:
def __init__(self, event, layout, background_file): def __init__(self, event, layout, background_file):

View File

@@ -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(): def restricted_plugin_kwargs():
from pretix.base.plugins import get_all_plugins from pretix.base.plugins import get_all_plugins
@@ -644,6 +656,19 @@ DEFAULTS = {
help_text=_("Only respected by some invoice renderers."), 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': { 'invoice_renderer': {
'default': 'classic', # default for new events is 'modern1' 'default': 'classic', # default for new events is 'modern1'
'type': str, 'type': str,

View File

@@ -857,6 +857,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
'invoice_eu_currencies', 'invoice_eu_currencies',
'invoice_logo_image', 'invoice_logo_image',
'invoice_renderer_highlight_order_code', 'invoice_renderer_highlight_order_code',
'invoice_renderer_font',
] ]
invoice_generate_sales_channels = forms.MultipleChoiceField( invoice_generate_sales_channels = forms.MultipleChoiceField(

View File

@@ -54,6 +54,7 @@
{% bootstrap_field form.invoice_additional_text layout="control" %} {% bootstrap_field form.invoice_additional_text layout="control" %}
{% bootstrap_field form.invoice_footer_text layout="control" %} {% bootstrap_field form.invoice_footer_text layout="control" %}
{% bootstrap_field form.invoice_logo_image 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_renderer_highlight_order_code layout="control" %}
{% bootstrap_field form.invoice_eu_currencies layout="control" %} {% bootstrap_field form.invoice_eu_currencies layout="control" %}
</fieldset> </fieldset>

View File

@@ -19,6 +19,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
from arabic_reshaper import ArabicReshaper
from django.utils.functional import SimpleLazyObject
from PIL.Image import Resampling from PIL.Image import Resampling
from reportlab.lib.utils import ImageReader 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 # file handle if the file is a JPEG, and therefore does not respect the
# (smaller) size of the modified image. # (smaller) size of the modified image.
return None return None
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
'delete_harakat': True,
'support_ligatures': False,
}))