diff --git a/src/db.sqlite3.bak b/src/db.sqlite3.bak
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/pretix/base/migrations/0012_auto_20160312_1040.py b/src/pretix/base/migrations/0012_auto_20160312_1040.py
new file mode 100644
index 0000000000..58f916feeb
--- /dev/null
+++ b/src/pretix/base/migrations/0012_auto_20160312_1040.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-03-12 10:40
+from __future__ import unicode_literals
+
+import datetime
+from decimal import Decimal
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+import pretix.base.models.invoices
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pretixbase', '0011_auto_20160311_2052'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Invoice',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('invoice_no', models.PositiveIntegerField(db_index=True)),
+ ('is_cancelled', models.BooleanField(default=False)),
+ ('invoice_from', models.TextField()),
+ ('invoice_to', models.TextField()),
+ ('date', models.DateField(default=datetime.date.today)),
+ ('file', models.FileField(blank=True, null=True, upload_to=pretix.base.models.invoices.invoice_filename)),
+ ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='pretixbase.Event')),
+ ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='pretixbase.Order')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='InvoiceLine',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('description', models.TextField()),
+ ('gross_value', models.DecimalField(decimal_places=2, max_digits=10)),
+ ('tax_value', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
+ ('tax_rate', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=7)),
+ ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='pretixbase.Invoice')),
+ ],
+ ),
+ migrations.AlterUniqueTogether(
+ name='invoice',
+ unique_together=set([('event', 'invoice_no')]),
+ ),
+ ]
diff --git a/src/pretix/base/migrations/0013_invoice_locale.py b/src/pretix/base/migrations/0013_invoice_locale.py
new file mode 100644
index 0000000000..e321e29343
--- /dev/null
+++ b/src/pretix/base/migrations/0013_invoice_locale.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-03-12 10:51
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pretixbase', '0012_auto_20160312_1040'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='invoice',
+ name='locale',
+ field=models.CharField(default='en', max_length=50),
+ ),
+ ]
diff --git a/src/pretix/base/migrations/0014_invoice_additional_text.py b/src/pretix/base/migrations/0014_invoice_additional_text.py
new file mode 100644
index 0000000000..594e458415
--- /dev/null
+++ b/src/pretix/base/migrations/0014_invoice_additional_text.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-03-12 11:07
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pretixbase', '0013_invoice_locale'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='invoice',
+ name='additional_text',
+ field=models.TextField(blank=True),
+ ),
+ ]
diff --git a/src/pretix/base/migrations/0015_auto_20160312_1924.py b/src/pretix/base/migrations/0015_auto_20160312_1924.py
new file mode 100644
index 0000000000..4eb137c26c
--- /dev/null
+++ b/src/pretix/base/migrations/0015_auto_20160312_1924.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-03-12 19:24
+from __future__ import unicode_literals
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pretixbase', '0014_invoice_additional_text'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='invoice',
+ name='is_cancelled',
+ ),
+ migrations.AddField(
+ model_name='invoice',
+ name='is_cancellation',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='invoice',
+ name='refers',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='refered', to='pretixbase.Invoice'),
+ ),
+ ]
diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py
index 7a0db98b34..18981e26a3 100644
--- a/src/pretix/base/models/__init__.py
+++ b/src/pretix/base/models/__init__.py
@@ -1,6 +1,7 @@
from .auth import User
from .base import CachedFile, cachedfile_name
from .event import Event, EventLock, EventPermission, EventSetting
+from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (
Item, ItemCategory, ItemVariation, Question, Quota, itempicture_upload_to,
)
@@ -17,5 +18,6 @@ __all__ = [
'ItemCategory', 'Item', 'Property', 'PropertyValue', 'ItemVariation', 'VariationsField', 'Question',
'BaseRestriction', 'Quota', 'Order', 'CachedTicket', 'QuestionAnswer', 'AbstractPosition', 'OrderPosition',
'CartPosition', 'EventSetting', 'OrganizerSetting', 'EventLock', 'cachedfile_name', 'itempicture_upload_to',
- 'generate_secret', 'Voucher', 'LogEntry', 'InvoiceAddress', 'generate_position_secret'
+ 'generate_secret', 'Voucher', 'LogEntry', 'InvoiceAddress', 'generate_position_secret', 'InvoiceLine',
+ 'Invoice', 'invoice_filename'
]
diff --git a/src/pretix/base/models/invoices.py b/src/pretix/base/models/invoices.py
new file mode 100644
index 0000000000..9bb6ea52f8
--- /dev/null
+++ b/src/pretix/base/models/invoices.py
@@ -0,0 +1,62 @@
+import random
+import string
+from datetime import date
+from decimal import Decimal
+
+from django.db import DatabaseError, models
+from django.db.models import Max
+
+
+def invoice_filename(instance, filename: str) -> str:
+ secret = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(14))
+ return 'invoices/{org}/{ev}/{ev}-{no}-{code}-{secret}.pdf'.format(
+ org=instance.event.organizer.slug, ev=instance.event.slug,
+ no=instance.invoice_no, code=instance.order.code,
+ secret=secret
+ )
+
+
+class Invoice(models.Model):
+ order = models.ForeignKey('Order', related_name='invoices', db_index=True)
+ event = models.ForeignKey('Event', related_name='invoices', db_index=True)
+ invoice_no = models.PositiveIntegerField(db_index=True)
+ is_cancellation = models.BooleanField(default=False)
+ refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True)
+ invoice_from = models.TextField()
+ invoice_to = models.TextField()
+ date = models.DateField(default=date.today)
+ locale = models.CharField(max_length=50, default='en')
+ additional_text = models.TextField(blank=True)
+ file = models.FileField(null=True, blank=True, upload_to=invoice_filename)
+
+ def save(self, *args, **kwargs):
+ if not self.order:
+ raise ValueError('Any invoice needs to be connected to an order')
+ if not self.event:
+ self.event = self.order.event
+ if not self.invoice_no:
+ for i in range(10):
+ self.invoice_no = (Invoice.objects.filter(
+ event=self.event).aggregate(m=Max('invoice_no'))['m'] or 0) + 1
+ try:
+ return super().save(*args, **kwargs)
+ except DatabaseError:
+ # Suppress duplicate key errors and try again
+ if i == 9:
+ raise
+ return super().save(*args, **kwargs)
+
+ @property
+ def number(self):
+ return '%s-%05d' % (self.event.slug.upper(), self.invoice_no)
+
+ class Meta:
+ unique_together = ('event', 'invoice_no')
+
+
+class InvoiceLine(models.Model):
+ invoice = models.ForeignKey('Invoice', related_name='lines')
+ description = models.TextField()
+ gross_value = models.DecimalField(max_digits=10, decimal_places=2)
+ tax_value = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
+ tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py
new file mode 100644
index 0000000000..5110893b81
--- /dev/null
+++ b/src/pretix/base/services/invoices.py
@@ -0,0 +1,338 @@
+import copy
+import tempfile
+from collections import defaultdict
+from datetime import date
+from decimal import Decimal
+from locale import format as lformat
+
+from django.conf import settings
+from django.contrib.staticfiles import finders
+from django.core.files.base import ContentFile
+from django.db import transaction
+from django.utils import translation
+from django.utils.formats import date_format
+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.pdfbase import pdfmetrics
+from reportlab.pdfbase.ttfonts import TTFont
+from reportlab.platypus import (
+ BaseDocTemplate, Frame, NextPageTemplate, PageTemplate, Paragraph, Spacer,
+ Table, TableStyle,
+)
+
+from pretix.base.models import Invoice, InvoiceAddress, InvoiceLine, Order
+from pretix.base.signals import register_payment_providers
+
+
+def generate_cancellation(invoice: Invoice):
+ cancellation = copy.copy(invoice)
+ cancellation.pk = None
+ cancellation.is_cancellation = True
+ cancellation.date = date.today()
+ cancellation.refers = invoice
+ cancellation.invoice_no = None
+ cancellation.save()
+ for line in invoice.lines.all():
+ line.pk = None
+ line.invoice = cancellation
+ line.gross_value *= -1
+ line.tax_value *= -1
+ line.save()
+
+ invoice_pdf(cancellation.pk)
+
+
+@transaction.atomic
+def generate_invoice(order: Order):
+ locale = order.event.settings.get('invoice_language')
+ _lng = translation.get_language()
+ if locale:
+ if locale == '__user__':
+ locale = order.locale
+ translation.activate(locale or settings.LANGUAGE_CODE)
+
+ i = Invoice(order=order, event=order.event)
+ i.invoice_from = order.event.settings.get('invoice_address_from')
+ i.additional_text = order.event.settings.get('invoice_additional_text')
+
+ try:
+ addr_template = pgettext("invoice", """{i.company}
+{i.name}
+{i.street}
+{i.zipcode} {i.city}
+{i.country}""")
+ i.invoice_to = addr_template.format(i=order.invoice_address).strip()
+ if order.invoice_address.vat_id:
+ i.invoice_to += "\n" + pgettext("invoice", "VAT-ID: %s") % {i.vat_id}
+ except InvoiceAddress.DoesNotExist:
+ i.invoice_to = ""
+
+ i.date = date.today()
+ i.locale = locale
+ i.save()
+
+ responses = register_payment_providers.send(order.event)
+ for receiver, response in responses:
+ provider = response(order.event)
+ if provider.identifier == order.payment_provider:
+ payment_provider = provider
+ break
+
+ for p in order.positions.all():
+ desc = str(p.item.name)
+ if p.variation:
+ desc += " - " + str(p.variation.value)
+ InvoiceLine.objects.create(
+ invoice=i, description=desc,
+ gross_value=p.price, tax_value=p.tax_value,
+ tax_rate=p.tax_rate
+ )
+
+ if order.payment_fee:
+ InvoiceLine.objects.create(
+ invoice=i, description=_('Payment via {method}').format(method=str(payment_provider.verbose_name)),
+ gross_value=order.payment_fee, tax_value=order.payment_fee_tax_value,
+ tax_rate=order.payment_fee_tax_rate
+ )
+
+ translation.activate(_lng)
+ invoice_pdf(i.pk)
+
+
+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,))
+ 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,))
+
+ textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
+ textobject.setFont('OpenSansBd', 8)
+ textobject.textLine(pgettext('invoice', 'Invoice from').upper())
+ textobject.moveCursor(0, 5)
+ textobject.setFont('OpenSans', 10)
+ textobject.textLines(invoice.invoice_from.strip())
+ canvas.drawText(textobject)
+
+ textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
+ textobject.setFont('OpenSansBd', 8)
+ textobject.textLine(pgettext('invoice', 'Invoice to').upper())
+ textobject.moveCursor(0, 5)
+ textobject.setFont('OpenSans', 10)
+ textobject.textLines(invoice.invoice_to.strip())
+ 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)
+
+ 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.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)
+
+ textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
+ textobject.setFont('OpenSansBd', 8)
+ textobject.textLine(_('Event').upper())
+ textobject.moveCursor(0, 5)
+ textobject.setFont('OpenSans', 10)
+ textobject.textLine(str(invoice.event.name))
+ if invoice.event.settings.show_date_to:
+ textobject.textLines(
+ _('%s\nuntil %s') % (invoice.event.get_date_from_display(),
+ invoice.event.get_date_to_display()))
+ else:
+ textobject.textLine(invoice.event.get_date_from_display())
+ canvas.drawText(textobject)
+
+ canvas.restoreState()
+
+ doc = BaseDocTemplate(f.name, pagesize=pagesizes.A4,
+ leftMargin=25 * mm, rightMargin=20 * mm,
+ topMargin=20 * mm, bottomMargin=15 * mm)
+ frames_p1 = [
+ Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 75 * mm,
+ leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0,
+ id='normal')
+ ]
+ frames = [
+ Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height,
+ leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0,
+ 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'),
+ ]
+
+ taxvalue_map = defaultdict(Decimal)
+ grossvalue_map = defaultdict(Decimal)
+
+ tstyledata = [
+ ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
+ ('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', 'Price'))]
+ total = Decimal('0.00')
+ for line in invoice.lines.all():
+ tdata.append((
+ line.description,
+ lformat("%.2f", line.tax_rate) + " %",
+ lformat("%.2f", 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'), '', lformat("%.2f", total) + " " + invoice.event.currency])
+ colwidths = [a * doc.width for a in (.60, .20, .20)]
+ table = Table(tdata, colWidths=colwidths, repeatRows=1)
+ table.setStyle(TableStyle(tstyledata))
+ story.append(table)
+
+ story.append(Spacer(1, 15 * mm))
+ 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((
+ '',
+ lformat("%.2f", rate) + " %",
+ lformat("%.2f", (gross - tax)) + " " + invoice.event.currency,
+ lformat("%.2f", gross) + " " + invoice.event.currency,
+ lformat("%.2f", 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
+
+
+def invoice_pdf(invoice: int):
+ i = Invoice.objects.get(pk=invoice)
+ _lng = translation.get_language()
+ translation.activate(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()
+
+ translation.activate(_lng)
+ return i.file.name
+
+
+if settings.HAS_CELERY:
+ from pretix.celery import app
+
+ invoice_pdf_task = app.task(invoice_pdf)
+
+ def invoice_pdf(*args, **kwargs):
+ invoice_pdf_task.apply_async(args=args, kwargs=kwargs)
diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py
index 25a8b1dfcf..609af75cb4 100644
--- a/src/pretix/base/services/orders.py
+++ b/src/pretix/base/services/orders.py
@@ -12,6 +12,9 @@ from pretix.base.models import (
)
from pretix.base.models.orders import InvoiceAddress
from pretix.base.payment import BasePaymentProvider
+from pretix.base.services.invoices import (
+ generate_cancellation, generate_invoice, invoice_pdf,
+)
from pretix.base.services.mail import mail
from pretix.base.signals import (
order_paid, order_placed, register_payment_providers,
@@ -99,6 +102,11 @@ def mark_order_refunded(order: Order, user: User=None):
order.status = Order.STATUS_REFUNDED
order.save()
order.log_action('pretix.event.order.refunded', user=user)
+
+ i = order.invoices.filter(is_cancellation=False).last()
+ if i:
+ generate_cancellation(i)
+
return order
@@ -112,6 +120,11 @@ def cancel_order(order: Order, user: User=None):
order.status = Order.STATUS_CANCELLED
order.save()
order.log_action('pretix.event.order.cancelled', user=user)
+
+ i = order.invoices.filter(is_cancellation=False).last()
+ if i:
+ generate_cancellation(i)
+
return order
@@ -250,6 +263,9 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
except InvoiceAddress.DoesNotExist:
pass
+ if event.settings.get('invoice_generate'):
+ generate_invoice(order)
+
mail(
order.email, _('Your order: %(code)s') % {'code': order.code},
event.settings.mail_text_order_placed,
diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py
index ac7819d452..9a4e61bb16 100644
--- a/src/pretix/base/settings.py
+++ b/src/pretix/base/settings.py
@@ -61,6 +61,22 @@ DEFAULTS = {
'default': '0.00',
'type': decimal.Decimal
},
+ 'invoice_generate': {
+ 'default': 'False',
+ 'type': bool
+ },
+ 'invoice_address_from': {
+ 'default': '',
+ 'type': str
+ },
+ 'invoice_additional_text': {
+ 'default': '',
+ 'type': str
+ },
+ 'invoice_language': {
+ 'default': '__user__',
+ 'type': str
+ },
'show_items_outside_presale_period': {
'default': 'True',
'type': bool
diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py
index e6df5a8292..e0060b91b9 100644
--- a/src/pretix/control/forms/event.py
+++ b/src/pretix/control/forms/event.py
@@ -148,6 +148,26 @@ class EventSettingsForm(SettingsForm):
help_text=_("Does only work if an invoice address is asked for. VAT ID is not required."),
required=False
)
+ invoice_generate = forms.BooleanField(
+ label=_("Generate invoices"),
+ required=False
+ )
+ invoice_address_from = forms.CharField(
+ widget=forms.Textarea(attrs={'rows': 5}), required=False,
+ label=_("Your address"),
+ help_text=_("Will be printed as the sender on invoices. Be sure to include relevant details required in "
+ "your jurisdiction (e.g. your VAT ID).")
+ )
+ invoice_additional_text = forms.CharField(
+ widget=forms.Textarea(attrs={'rows': 5}), required=False,
+ label=_("Additional text"),
+ help_text=_("Will be printed on every invoice below the invoice total.")
+ )
+ invoice_language = forms.ChoiceField(
+ widget=forms.Select, required=True,
+ label=_("Invoice language"),
+ choices=[('__user__', _('The user\'s language'))] + settings.LANGUAGES,
+ )
max_items_per_order = forms.IntegerField(
min_value=1,
label=_("Maximum number of items per order")
diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html
index a9ae5e785c..773dc47eb8 100644
--- a/src/pretix/control/templates/pretixcontrol/event/settings.html
+++ b/src/pretix/control/templates/pretixcontrol/event/settings.html
@@ -48,6 +48,10 @@
{% bootstrap_field sform.invoice_address_required layout="horizontal" %}
{% bootstrap_field sform.invoice_address_vatid layout="horizontal" %}
{% bootstrap_field sform.tax_rate_default layout="horizontal" %}
+ {% bootstrap_field sform.invoice_generate layout="horizontal" %}
+ {% bootstrap_field sform.invoice_language layout="horizontal" %}
+ {% bootstrap_field sform.invoice_address_from layout="horizontal" %}
+ {% bootstrap_field sform.invoice_additional_text layout="horizontal" %}
diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py
index 7a7e45c132..5f422729fb 100644
--- a/src/pretix/control/urls.py
+++ b/src/pretix/control/urls.py
@@ -70,6 +70,8 @@ urlpatterns = [
url(r'^orders/(?P[0-9A-Z]+)/$', orders.OrderDetail.as_view(), name='event.order'),
url(r'^orders/(?P[0-9A-Z]+)/download/(?P