import copy import tempfile from collections import defaultdict from datetime import date 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.formats import date_format, localize from django.utils.timezone import now from django.utils.translation import pgettext, ugettext as _ 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 LazyI18nString, language from pretix.base.models import Invoice, InvoiceAddress, InvoiceLine, Order from pretix.base.services.async import TransactionAwareTask from pretix.base.signals import register_payment_providers from pretix.celery_app import app from pretix.helpers.database import rolledback_transaction @transaction.atomic def build_invoice(invoice: Invoice) -> Invoice: with language(invoice.locale): responses = register_payment_providers.send(invoice.event) for receiver, response in responses: provider = response(invoice.event) if provider.identifier == invoice.order.payment_provider: payment_provider = provider break invoice.invoice_from = invoice.event.settings.get('invoice_address_from') introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString) additional = invoice.event.settings.get('invoice_additional_text', as_type=LazyI18nString) footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString) payment = payment_provider.render_invoice_text(invoice.order) invoice.introductory_text = str(introductory).replace('\n', '
') invoice.additional_text = str(additional).replace('\n', '
') invoice.footer_text = str(footer) invoice.payment_provider_text = str(payment).replace('\n', '
') try: addr_template = pgettext("invoice", """{i.company} {i.name} {i.street} {i.zipcode} {i.city} {i.country}""") invoice.invoice_to = addr_template.format(i=invoice.order.invoice_address).strip() if invoice.order.invoice_address.vat_id: invoice.invoice_to += "\n" + pgettext("invoice", "VAT-ID: %s") % invoice.order.invoice_address.vat_id except InvoiceAddress.DoesNotExist: invoice.invoice_to = "" invoice.file = None invoice.save() invoice.lines.all().delete() for p in invoice.order.positions.all(): desc = str(p.item.name) if p.variation: desc += " - " + str(p.variation.value) InvoiceLine.objects.create( invoice=invoice, description=desc, gross_value=p.price, tax_value=p.tax_value, tax_rate=p.tax_rate ) if invoice.order.payment_fee: InvoiceLine.objects.create( 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 ) return invoice def build_cancellation(invoice: Invoice): invoice.lines.all().delete() for line in invoice.refers.lines.all(): line.pk = None line.invoice = invoice line.gross_value *= -1 line.tax_value *= -1 line.save() return invoice def generate_cancellation(invoice: Invoice): cancellation = copy.copy(invoice) cancellation.pk = None cancellation.invoice_no = None cancellation.refers = invoice cancellation.is_cancellation = True cancellation.date = date.today() cancellation.payment_provider_text = '' cancellation.save() cancellation = build_cancellation(cancellation) invoice_pdf(cancellation.pk) return cancellation def regenerate_invoice(invoice: Invoice): if invoice.is_cancellation: invoice = build_cancellation(invoice) else: invoice = build_invoice(invoice) invoice_pdf(invoice.pk) return invoice def generate_invoice(order: Order): locale = order.event.settings.get('invoice_language') if locale: if locale == '__user__': locale = order.locale invoice = Invoice( order=order, event=order.event, date=date.today(), locale=locale ) invoice = build_invoice(invoice) invoice_pdf(invoice.pk) 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', '
\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', '
\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 - 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) textobject = canvas.beginText(165 * mm, (297 - 50) * mm) textobject.setFont('OpenSansBd', 8) textobject.textLine(_('Order code').upper()) textobject.moveCursor(0, 5) textobject.setFont('OpenSans', 10) textobject.textLine(invoice.order.full_code) textobject.moveCursor(0, 5) textobject.setFont('OpenSansBd', 8) textobject.textLine(_('Order date').upper()) textobject.moveCursor(0, 5) textobject.setFont('OpenSans', 10) textobject.textLine(date_format(invoice.order.datetime, "DATE_FORMAT")) 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') 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', '
\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())) i.save() return i.file.name def invoice_qualified(order: Order): if order.total == Decimal('0.00'): return False return True def invoice_pdf(*args, **kwargs): # We call this task asynchroneously, because otherwise we run into conditions where # the task worker tries to generate the PDF even before our database transaction # was committed and therefore fails to find the invoice object. The invoice_pdf_task # will prevent this kind of race condition. invoice_pdf_task.apply_async(args=args, kwargs=kwargs) class DummyRollbackException(Exception): pass def build_preview_invoice_pdf(event): locale = event.settings.invoice_language if not locale or locale == '__user__': locale = event.settings.locale with rolledback_transaction(), language(locale): order = event.orders.create(status=Order.STATUS_PENDING, datetime=now(), expires=now(), code="PREVIEW", total=119) invoice = Invoice( order=order, event=event, invoice_no="PREVIEW", date=date.today(), locale=locale ) invoice.invoice_from = event.settings.get('invoice_address_from') introductory = event.settings.get('invoice_introductory_text', as_type=LazyI18nString) additional = event.settings.get('invoice_additional_text', as_type=LazyI18nString) footer = event.settings.get('invoice_footer_text', as_type=LazyI18nString) payment = _("A payment provider specific text might appear here.") invoice.introductory_text = str(introductory).replace('\n', '
') invoice.additional_text = str(additional).replace('\n', '
') invoice.footer_text = str(footer) invoice.payment_provider_text = str(payment).replace('\n', '
') invoice.invoice_to = _("John Doe\n214th Example Street\n012345 Somecity") invoice.file = None invoice.save() invoice.lines.all().delete() InvoiceLine.objects.create( invoice=invoice, description=_("Sample product A"), 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()