Swappable invoice renderers

This commit is contained in:
Raphael Michel
2017-07-07 11:16:07 +02:00
parent 95e716b8ce
commit 6e65ae5306
12 changed files with 564 additions and 279 deletions

View File

@@ -1,25 +1,11 @@
import copy
import tempfile
from collections import defaultdict
from decimal import Decimal
from django.contrib.staticfiles import finders
from django.core.files.base import ContentFile
from django.db import transaction
from django.utils import timezone
from django.utils.formats import date_format, localize
from django.utils.translation import pgettext, ugettext as _
from i18nfield.strings import LazyI18nString
from reportlab.lib import pagesizes
from reportlab.lib.styles import ParagraphStyle, StyleSheet1
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (
BaseDocTemplate, Frame, NextPageTemplate, PageTemplate, Paragraph, Spacer,
Table, TableStyle,
)
from pretix.base.i18n import language
from pretix.base.models import Invoice, InvoiceAddress, InvoiceLine, Order
@@ -77,7 +63,8 @@ def build_invoice(invoice: Invoice) -> Invoice:
if invoice.order.payment_fee:
InvoiceLine.objects.create(
invoice=invoice, description=_('Payment via {method}').format(method=str(payment_provider.verbose_name)),
invoice=invoice,
description=_('Payment via {method}').format(method=str(payment_provider.verbose_name)),
gross_value=invoice.order.payment_fee, tax_value=invoice.order.payment_fee_tax_value,
tax_rate=invoice.order.payment_fee_tax_rate
)
@@ -138,265 +125,12 @@ def generate_invoice(order: Order):
return invoice
def _invoice_get_stylesheet():
stylesheet = StyleSheet1()
stylesheet.add(ParagraphStyle(name='Normal', fontName='OpenSans', fontSize=10, leading=12))
stylesheet.add(ParagraphStyle(name='Heading1', fontName='OpenSansBd', fontSize=15, leading=15 * 1.2))
return stylesheet
def _invoice_register_fonts():
pdfmetrics.registerFont(TTFont('OpenSans', finders.find('fonts/OpenSans-Regular.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansIt', finders.find('fonts/OpenSans-Italic.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansBd', finders.find('fonts/OpenSans-Bold.ttf')))
def _invoice_generate_german(invoice, f):
_invoice_register_fonts()
styles = _invoice_get_stylesheet()
pagesize = pagesizes.A4
def on_page(canvas, doc):
canvas.saveState()
canvas.setFont('OpenSans', 8)
canvas.drawRightString(pagesize[0] - 20 * mm, 10 * mm, _("Page %d") % (doc.page,))
for i, line in enumerate(invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
canvas.restoreState()
def on_first_page(canvas, doc):
canvas.setCreator('pretix.eu')
canvas.setTitle(pgettext('invoice', 'Invoice {num}').format(num=invoice.number))
canvas.saveState()
canvas.setFont('OpenSans', 8)
canvas.drawRightString(pagesize[0] - 20 * mm, 10 * mm, _("Page %d") % (doc.page,))
for i, line in enumerate(invoice.footer_text.split('\n')[::-1]):
canvas.drawCentredString(pagesize[0] / 2, 25 + (3.5 * i) * mm, line.strip())
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice from').upper())
canvas.drawText(textobject)
p = Paragraph(invoice.invoice_from.strip().replace('\n', '<br />\n'), style=styles['Normal'])
p.wrapOn(canvas, 70 * mm, 50 * mm)
p_size = p.wrap(70 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 17) * mm - p_size[1])
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice to').upper())
canvas.drawText(textobject)
p = Paragraph(invoice.invoice_to.strip().replace('\n', '<br />\n'), style=styles['Normal'])
p.wrapOn(canvas, 85 * mm, 50 * mm)
p_size = p.wrap(85 * mm, 50 * mm)
p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1])
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(_('Order code').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(invoice.order.full_code)
canvas.drawText(textobject)
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
textobject.setFont('OpenSansBd', 8)
if invoice.is_cancellation:
textobject.textLine(pgettext('invoice', 'Cancellation number').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(invoice.number)
textobject.moveCursor(0, 5)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Original invoice').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(invoice.refers.number)
else:
textobject.textLine(pgettext('invoice', 'Invoice number').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(invoice.number)
textobject.moveCursor(0, 5)
if invoice.is_cancellation:
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Cancellation date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Original invoice date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(invoice.refers.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
else:
textobject.setFont('OpenSansBd', 8)
textobject.textLine(pgettext('invoice', 'Invoice date').upper())
textobject.moveCursor(0, 5)
textobject.setFont('OpenSans', 10)
textobject.textLine(date_format(invoice.date, "DATE_FORMAT"))
textobject.moveCursor(0, 5)
canvas.drawText(textobject)
if invoice.event.settings.invoice_logo_image:
logo_file = invoice.event.settings.get('invoice_logo_image', binary_file=True)
canvas.drawImage(ImageReader(logo_file),
95 * mm, (297 - 38) * mm,
width=25 * mm, height=25 * mm,
preserveAspectRatio=True, anchor='n',
mask='auto')
if invoice.event.settings.show_date_to:
p_str = (
str(invoice.event.name) + '\n' + _('{from_date}\nuntil {to_date}').format(
from_date=invoice.event.get_date_from_display(),
to_date=invoice.event.get_date_to_display())
)
else:
p_str = (
str(invoice.event.name) + '\n' + invoice.event.get_date_from_display()
)
p = Paragraph(p_str.strip().replace('\n', '<br />\n'), style=styles['Normal'])
p.wrapOn(canvas, 65 * mm, 50 * mm)
p_size = p.wrap(65 * mm, 50 * mm)
p.drawOn(canvas, 125 * mm, (297 - 17) * mm - p_size[1])
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
textobject.setFont('OpenSansBd', 8)
textobject.textLine(_('Event').upper())
canvas.drawText(textobject)
canvas.restoreState()
doc = BaseDocTemplate(f.name, pagesize=pagesizes.A4,
leftMargin=25 * mm, rightMargin=20 * mm,
topMargin=20 * mm, bottomMargin=15 * mm)
footer_length = 3.5 * len(invoice.footer_text.split('\n')) * mm
frames_p1 = [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 75 * mm,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
frames = [
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height,
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
id='normal')
]
doc.addPageTemplates([
PageTemplate(id='FirstPage', frames=frames_p1, onPage=on_first_page, pagesize=pagesize),
PageTemplate(id='OtherPages', frames=frames, onPage=on_page, pagesize=pagesize)
])
story = [
NextPageTemplate('FirstPage'),
Paragraph(pgettext('invoice', 'Invoice')
if not invoice.is_cancellation
else pgettext('invoice', 'Cancellation'),
styles['Heading1']),
Spacer(1, 5 * mm),
NextPageTemplate('OtherPages'),
]
if invoice.introductory_text:
story.append(Paragraph(invoice.introductory_text, styles['Normal']))
story.append(Spacer(1, 10 * mm))
taxvalue_map = defaultdict(Decimal)
grossvalue_map = defaultdict(Decimal)
tstyledata = [
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('FONTNAME', (0, 0), (-1, 0), 'OpenSansBd'),
('FONTNAME', (0, -1), (-1, -1), 'OpenSansBd'),
('LEFTPADDING', (0, 0), (0, -1), 0),
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
]
tdata = [(
pgettext('invoice', 'Description'),
pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net'),
pgettext('invoice', 'Gross'),
)]
total = Decimal('0.00')
for line in invoice.lines.all():
tdata.append((
Paragraph(line.description, styles['Normal']),
localize(line.tax_rate) + " %",
localize(line.net_value) + " " + invoice.event.currency,
localize(line.gross_value) + " " + invoice.event.currency,
))
taxvalue_map[line.tax_rate] += line.tax_value
grossvalue_map[line.tax_rate] += line.gross_value
total += line.gross_value
tdata.append([pgettext('invoice', 'Invoice total'), '', '', localize(total) + " " + invoice.event.currency])
colwidths = [a * doc.width for a in (.55, .15, .15, .15)]
table = Table(tdata, colWidths=colwidths, repeatRows=1)
table.setStyle(TableStyle(tstyledata))
story.append(table)
story.append(Spacer(1, 15 * mm))
if invoice.payment_provider_text:
story.append(Paragraph(invoice.payment_provider_text, styles['Normal']))
if invoice.additional_text:
story.append(Paragraph(invoice.additional_text, styles['Normal']))
story.append(Spacer(1, 15 * mm))
tstyledata = [
('SPAN', (1, 0), (-1, 0)),
('ALIGN', (2, 1), (-1, -1), 'RIGHT'),
('LEFTPADDING', (0, 0), (0, -1), 0),
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
('FONTSIZE', (0, 0), (-1, -1), 8),
]
tdata = [('', pgettext('invoice', 'Included taxes'), '', '', ''),
('', pgettext('invoice', 'Tax rate'),
pgettext('invoice', 'Net value'), pgettext('invoice', 'Gross value'), pgettext('invoice', 'Tax'))]
for rate, gross in grossvalue_map.items():
if line.tax_rate == 0:
continue
tax = taxvalue_map[rate]
tdata.append((
'',
localize(rate) + " %",
localize((gross - tax)) + " " + invoice.event.currency,
localize(gross) + " " + invoice.event.currency,
localize(tax) + " " + invoice.event.currency,
))
if len(tdata) > 2:
colwidths = [a * doc.width for a in (.45, .10, .15, .15, .15)]
table = Table(tdata, colWidths=colwidths, repeatRows=2)
table.setStyle(TableStyle(tstyledata))
story.append(table)
doc.build(story)
return doc
@app.task(base=TransactionAwareTask)
def invoice_pdf_task(invoice: int):
i = Invoice.objects.get(pk=invoice)
with language(i.locale):
with tempfile.NamedTemporaryFile(suffix=".pdf") as f:
_invoice_generate_german(i, f)
f.seek(0)
i.file.save('invoice.pdf', ContentFile(f.read()))
fname, ftype, fcontent = i.event.invoice_renderer.generate(i)
i.file.save(fname, ContentFile(fcontent))
i.save()
return i.file.name
@@ -452,7 +186,4 @@ def build_preview_invoice_pdf(event):
gross_value=119, tax_value=19,
tax_rate=19
)
with tempfile.NamedTemporaryFile(suffix=".pdf") as f:
_invoice_generate_german(invoice, f)
f.seek(0)
return f.read()
return event.invoice_renderer.generate(invoice)