Refs #131 -- Basic implementation of invoicing

This commit is contained in:
Raphael Michel
2016-03-12 12:03:00 +01:00
parent 0f054416fc
commit 5ab78b4576
21 changed files with 1489 additions and 374 deletions

0
src/db.sqlite3.bak Normal file
View File

View File

@@ -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')]),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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'),
),
]

View File

@@ -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'
]

View File

@@ -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'))

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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")

View File

@@ -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" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -77,6 +77,19 @@
</button>
</form>
</dd>
{% if invoices %}
<dt>{% trans "Invoices" %}</dt>
<dd>
{% for i in invoices %}
<a href="{% url "control:event.invoice.download" invoice=i.pk event=request.event.slug organizer=request.event.organizer.slug %}">
{% if i.is_cancellation %}{% trans "Cancellation" %}{% else %}{% trans "Invoice" %}{% endif %}
{{ i.number }}</a> ({{ i.date|date:"SHORT_DATE_FORMAT" }})
{% if forloop.revcounter0 > 0 %}
<br />
{% endif %}
{% endfor %}
</dd>
{% endif %}
</dl>
</div>
</div>

View File

@@ -70,6 +70,8 @@ urlpatterns = [
url(r'^orders/(?P<code>[0-9A-Z]+)/$', orders.OrderDetail.as_view(), name='event.order'),
url(r'^orders/(?P<code>[0-9A-Z]+)/download/(?P<output>[^/]+)$', orders.OrderDownload.as_view(),
name='event.order.download'),
url(r'^invoice/(?P<invoice>[^/]+)$', orders.InvoiceDownload.as_view(),
name='event.invoice.download'),
url(r'^orders/overview/$', orders.OverView.as_view(), name='event.orders.overview'),
url(r'^orders/export/$', orders.ExportView.as_view(), name='event.orders.export'),
url(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),

View File

@@ -5,7 +5,7 @@ from django import forms
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.http import HttpResponse, HttpResponseNotAllowed
from django.http import Http404, HttpResponseNotAllowed
from django.shortcuts import redirect, render
from django.utils.functional import cached_property
from django.utils.timezone import now
@@ -13,10 +13,11 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, ListView, TemplateView, View
from pretix.base.models import (
CachedFile, CachedTicket, EventLock, Item, Order, Quota,
CachedFile, CachedTicket, EventLock, Invoice, Item, Order, Quota,
)
from pretix.base.services import tickets
from pretix.base.services.export import export
from pretix.base.services.invoices import invoice_pdf
from pretix.base.services.mail import mail
from pretix.base.services.orders import cancel_order, mark_order_paid
from pretix.base.services.stats import order_overview
@@ -118,6 +119,7 @@ class OrderDetail(OrderView):
and self.order.status == Order.STATUS_PAID
)
ctx['payment'] = self.payment_provider.order_control_render(self.request, self.object)
ctx['invoices'] = list(self.order.invoices.all())
return ctx
def get_items(self):
@@ -137,8 +139,8 @@ class OrderDetail(OrderView):
def keyfunc(pos):
if (pos.item.admission and self.request.event.settings.attendee_names_asked) \
or pos.item.questions.all():
return pos.id, 0, 0, 0, None
return 0, pos.item_id, pos.variation_id, pos.price, pos.voucher
return pos.id, 0, 0, 0, 0, None
return 0, pos.item_id, pos.variation_id, pos.price, pos.tax_rate, pos.voucher
positions = []
for k, g in groupby(sorted(list(cartpos), key=keyfunc), key=keyfunc):
@@ -221,6 +223,38 @@ class OrderResendLink(OrderView):
return redirect(self.get_order_url())
class InvoiceDownload(EventPermissionRequiredMixin, View):
permission = 'can_view_orders'
def get_order_url(self):
return reverse('control:event.order', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'code': self.invoice.order.code
})
def get(self, request, *args, **kwargs):
try:
self.invoice = Invoice.objects.get(
event=self.request.event,
id=self.kwargs['invoice']
)
except Invoice.DoesNotExist:
raise Http404(_('This invoice has not been found'))
if not self.invoice.file:
invoice_pdf(self.invoice.pk)
self.invoice = Invoice.objects.get(pk=self.invoice.pk)
if not self.invoice.file:
# This happens if we have celery installed and the file will be generated in the background
messages.warning(request, _('The invoice file has not yet been generated, we will generate it for you '
'now. Please try again in a few seconds.'))
return redirect(self.get_order_url())
return redirect(self.invoice.file.url)
class OrderDownload(OrderView):
@cached_property

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -91,43 +91,70 @@
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=False %}
</div>
</div>
{% if request.event.settings.invoice_address_asked %}
<div class="panel panel-primary">
<div class="panel-heading">
{% if order.can_modify_answers %}
<div class="pull-right">
<a href="{% eventurl event "presale:event.order.modify" secret=order.secret order=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Change details" %}
</a>
<div class="row">
{% if invoices %}
<div class="col-xs-12 {% if request.event.settings.invoice_address_asked %}col-md-6{% endif %}">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Invoices" %}
</h3>
</div>
{% endif %}
<h3 class="panel-title">
{% trans "Invoice information" %}
</h3>
<div class="panel-body">
<ul>
{% for i in invoices %}
<li>
<a href="{% eventurl event "presale:event.invoice.download" invoice=i.pk secret=order.secret order=order.code %}">
{% if i.is_cancellation %}{% trans "Cancellation" %}{% else %}{% trans "Invoice" %}{% endif %}
{{ i.number }}</a> ({{ i.date|date:"SHORT_DATE_FORMAT" }})
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>{% trans "Company" %}</dt>
<dd>{{ order.invoice_address.company }}</dd>
<dt>{% trans "Name" %}</dt>
<dd>{{ order.invoice_address.name }}</dd>
<dt>{% trans "Address" %}</dt>
<dd>{{ order.invoice_address.street|linebreaksbr }}</dd>
<dt>{% trans "ZIP code and city" %}</dt>
<dd>{{ order.invoice_address.zipcode }} {{ addr.city }}</dd>
<dt>{% trans "Country" %}</dt>
<dd>{{ order.invoice_address.country }}</dd>
<dt>{% trans "Phone" %}</dt>
<dd>{{ order.invoice_address.phone }}</dd>
{% if request.event.settings.invoice_address_vatid %}
<dt>{% trans "VAT ID" %}</dt>
<dd>{{ order.invoice_address.vat_id }}</dd>
{% endif %}
</dl>
{% endif %}
{% if request.event.settings.invoice_address_asked %}
<div class="col-xs-12 {% if invoices %}col-md-6{% endif %}">
<div class="panel panel-primary">
<div class="panel-heading">
{% if order.can_modify_answers %}
<div class="pull-right">
<a href="{% eventurl event "presale:event.order.modify" secret=order.secret order=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Change details" %}
</a>
</div>
{% endif %}
<h3 class="panel-title">
{% trans "Invoice information" %}
</h3>
</div>
<div class="panel-body">
<dl class="dl-horizontal">
<dt>{% trans "Company" %}</dt>
<dd>{{ order.invoice_address.company }}</dd>
<dt>{% trans "Name" %}</dt>
<dd>{{ order.invoice_address.name }}</dd>
<dt>{% trans "Address" %}</dt>
<dd>{{ order.invoice_address.street|linebreaksbr }}</dd>
<dt>{% trans "ZIP code and city" %}</dt>
<dd>{{ order.invoice_address.zipcode }} {{ addr.city }}</dd>
<dt>{% trans "Country" %}</dt>
<dd>{{ order.invoice_address.country }}</dd>
<dt>{% trans "Phone" %}</dt>
<dd>{{ order.invoice_address.phone }}</dd>
{% if request.event.settings.invoice_address_vatid %}
<dt>{% trans "VAT ID" %}</dt>
<dd>{{ order.invoice_address.vat_id }}</dd>
{% endif %}
</dl>
</div>
</div>
</div>
</div>
{% endif %}
{% endif %}
<div class="clearfix"></div>
</div>
{% if order.status == "n" %}
<div class="row">
<div class="col-md-12 text-right">

View File

@@ -35,6 +35,9 @@ event_patterns = [
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<output>[^/]+)$',
pretix.presale.views.order.OrderDownload.as_view(),
name='event.order.download'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/invoice/(?P<invoice>[^/]+)$',
pretix.presale.views.order.InvoiceDownload.as_view(),
name='event.invoice.download'),
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
]

View File

@@ -9,8 +9,11 @@ from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, View
from pretix.base.models import CachedFile, CachedTicket, Order, OrderPosition
from pretix.base.models import (
CachedFile, CachedTicket, Invoice, Order, OrderPosition,
)
from pretix.base.models.orders import InvoiceAddress
from pretix.base.services.invoices import invoice_pdf
from pretix.base.services.orders import cancel_order
from pretix.base.services.tickets import generate
from pretix.base.signals import (
@@ -85,6 +88,7 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
answers=True,
queryset=OrderPosition.objects.filter(order=self.order)
)
ctx['invoices'] = list(self.order.invoices.all())
if self.order.status == Order.STATUS_PENDING:
ctx['payment'] = self.payment_provider.order_pending_render(self.request, self.order)
ctx['can_retry'] = (
@@ -319,3 +323,31 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View):
ct.save()
generate(self.order.id, self.output.identifier)
return redirect(reverse('cachedfile.download', kwargs={'id': ct.cachedfile.id}))
class InvoiceDownload(EventViewMixin, OrderDetailMixin, View):
def get(self, request, *args, **kwargs):
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
try:
invoice = Invoice.objects.get(
event=self.request.event,
order=self.order,
id=self.kwargs['invoice']
)
except Invoice.DoesNotExist:
raise Http404(_('This invoice has not been found'))
if not invoice.file:
invoice_pdf(invoice.pk)
invoice = Invoice.objects.get(pk=invoice.pk)
if not invoice.file:
# This happens if we have celery installed and the file will be generated in the background
messages.warning(request, _('The invoice file has not yet been generated, we will generate it for you '
'now. Please try again in a few seconds.'))
return redirect(self.get_order_url())
return redirect(invoice.file.url)

View File

@@ -204,11 +204,11 @@ LOCALE_PATHS = (
os.path.join(os.path.dirname(__file__), 'locale'),
)
LANGUAGES = (
LANGUAGES = [
('en', _('English')),
('de', _('German')),
('de-informal', _('German (informal)')),
)
]
EXTRA_LANG_INFO = {
'de-informal': {