Merge pull request #225 from rixx/invoice_string_from_payment_provider

Invoice string from payment provider
This commit is contained in:
Raphael Michel
2016-09-08 18:32:28 +02:00
committed by GitHub
11 changed files with 128 additions and 112 deletions

View File

@@ -62,6 +62,8 @@ The provider class
.. automethod:: settings_content_render .. automethod:: settings_content_render
.. automethod:: render_invoice_text
.. automethod:: payment_form_render .. automethod:: payment_form_render
.. automethod:: payment_form .. automethod:: payment_form

View File

@@ -29,7 +29,7 @@ Logging form actions
A very common use case is to log the changes to a model that have been done in a ``ModelForm``. In this case, A very common use case is to log the changes to a model that have been done in a ``ModelForm``. In this case,
we generally use a custom ``form_valid`` method on our ``FormView`` that looks like this:: we generally use a custom ``form_valid`` method on our ``FormView`` that looks like this::
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
if form.has_changed(): if form.has_changed():
self.request.event.log_action('pretix.event.changed', user=self.request.user, data={ self.request.event.log_action('pretix.event.changed', user=self.request.user, data={
@@ -40,7 +40,7 @@ we generally use a custom ``form_valid`` method on our ``FormView`` that looks l
It gets a little bit more complicated if your form allows file uploads:: It gets a little bit more complicated if your form allows file uploads::
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
if form.has_changed(): if form.has_changed():
self.request.event.log_action( self.request.event.log_action(

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.9 on 2016-09-06 21:31
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0036_auto_20160902_0755'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='payment_provider_text',
field=models.TextField(blank=True),
),
]

View File

@@ -43,6 +43,8 @@ class Invoice(models.Model):
:type introductory_text: str :type introductory_text: str
:param additional_text: Additional text for the invoice :param additional_text: Additional text for the invoice
:type additional_text: str :type additional_text: str
:param payment_provider_text: A payment provider specific text
:type payment_provider_text: str
:param footer_text: A footer text, displayed smaller and centered on every page :param footer_text: A footer text, displayed smaller and centered on every page
:type footer_text: str :type footer_text: str
:param file: The filename of the rendered invoice :param file: The filename of the rendered invoice
@@ -59,6 +61,7 @@ class Invoice(models.Model):
locale = models.CharField(max_length=50, default='en') locale = models.CharField(max_length=50, default='en')
introductory_text = models.TextField(blank=True) introductory_text = models.TextField(blank=True)
additional_text = models.TextField(blank=True) additional_text = models.TextField(blank=True)
payment_provider_text = models.TextField(blank=True)
footer_text = models.TextField(blank=True) footer_text = models.TextField(blank=True)
file = models.FileField(null=True, blank=True, upload_to=invoice_filename) file = models.FileField(null=True, blank=True, upload_to=invoice_filename)

View File

@@ -12,6 +12,7 @@ from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pretix.base.decimal import round_decimal from pretix.base.decimal import round_decimal
from pretix.base.i18n import I18nFormField, I18nTextarea, LazyI18nString
from pretix.base.models import CartPosition, Event, Order, Quota from pretix.base.models import CartPosition, Event, Order, Quota
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
from pretix.base.signals import register_payment_providers from pretix.base.signals import register_payment_providers
@@ -134,6 +135,13 @@ class BasePaymentProvider:
'above!'), 'above!'),
required=False required=False
)), )),
('_invoice_text',
I18nFormField(
label=_('Text on invoices'),
help_text=_('Will be printed just below the payment figures and above the closing text on invoices.'),
required=False,
widget=I18nTextarea,
)),
]) ])
def settings_content_render(self, request: HttpRequest) -> str: def settings_content_render(self, request: HttpRequest) -> str:
@@ -144,6 +152,14 @@ class BasePaymentProvider:
""" """
pass pass
def render_invoice_text(self, order: Order) -> str:
"""
This is called when an invoice for an order with this payment provider is generated.
The default implementation returns the content of the _invoice_text configuration
variable (an I18nString), or an empty string if unconfigured.
"""
return self.settings.get('_invoice_text', as_type=LazyI18nString, default='')
@property @property
def payment_form_fields(self) -> dict: def payment_form_fields(self) -> dict:
""" """

View File

@@ -26,37 +26,27 @@ from pretix.base.models import Invoice, InvoiceAddress, InvoiceLine, Order
from pretix.base.signals import register_payment_providers 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)
return cancellation
@transaction.atomic @transaction.atomic
def regenerate_invoice(invoice: Invoice): def build_invoice(invoice: Invoice) -> Invoice:
with language(invoice.locale): 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') invoice.invoice_from = invoice.event.settings.get('invoice_address_from')
introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString) introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString)
additional = invoice.event.settings.get('invoice_additional_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) 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', '<br />') invoice.introductory_text = str(introductory).replace('\n', '<br />')
invoice.additional_text = str(additional).replace('\n', '<br /') invoice.additional_text = str(additional).replace('\n', '<br />')
invoice.footer_text = str(footer) invoice.footer_text = str(footer)
invoice.payment_provider_text = str(payment).replace('\n', '<br />')
try: try:
addr_template = pgettext("invoice", """{i.company} addr_template = pgettext("invoice", """{i.company}
@@ -72,15 +62,8 @@ def regenerate_invoice(invoice: Invoice):
invoice.file = None invoice.file = None
invoice.save() invoice.save()
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.lines.all().delete() invoice.lines.all().delete()
for p in invoice.order.positions.all(): for p in invoice.order.positions.all():
desc = str(p.item.name) desc = str(p.item.name)
if p.variation: if p.variation:
@@ -98,71 +81,60 @@ def regenerate_invoice(invoice: Invoice):
tax_rate=invoice.order.payment_fee_tax_rate tax_rate=invoice.order.payment_fee_tax_rate
) )
invoice_pdf(invoice.pk) 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 return invoice
@transaction.atomic
def generate_invoice(order: Order): def generate_invoice(order: Order):
locale = order.event.settings.get('invoice_language') locale = order.event.settings.get('invoice_language')
if locale: if locale:
if locale == '__user__': if locale == '__user__':
locale = order.locale locale = order.locale
with language(locale): invoice = Invoice(
i = Invoice(order=order, event=order.event) order=order,
i.invoice_from = order.event.settings.get('invoice_address_from') event=order.event,
date=date.today(),
introductory = i.event.settings.get('invoice_introductory_text', as_type=LazyI18nString) locale=locale
additional = i.event.settings.get('invoice_additional_text', as_type=LazyI18nString) )
footer = i.event.settings.get('invoice_footer_text', as_type=LazyI18nString) invoice = build_invoice(invoice)
invoice_pdf(invoice.pk)
i.introductory_text = str(introductory).replace('\n', '<br />') return invoice
i.additional_text = str(additional).replace('\n', '<br /')
i.footer_text = str(footer)
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") % order.invoice_address.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
)
invoice_pdf(i.pk)
return i
def _invoice_get_stylesheet(): def _invoice_get_stylesheet():
@@ -362,6 +334,9 @@ def _invoice_generate_german(invoice, f):
story.append(Spacer(1, 15 * mm)) story.append(Spacer(1, 15 * mm))
if invoice.payment_provider_text:
story.append(Paragraph(invoice.payment_provider_text, styles['Normal']))
if invoice.additional_text: if invoice.additional_text:
story.append(Paragraph(invoice.additional_text, styles['Normal'])) story.append(Paragraph(invoice.additional_text, styles['Normal']))
story.append(Spacer(1, 15 * mm)) story.append(Spacer(1, 15 * mm))

View File

@@ -80,7 +80,7 @@ def lock_event_db(event):
raise EventLock.LockTimeoutException() raise EventLock.LockTimeoutException()
@transaction.atomic() @transaction.atomic
def release_event_db(event): def release_event_db(event):
if not hasattr(event, '_lock') or not event._lock: if not hasattr(event, '_lock') or not event._lock:
raise EventLock.LockReleaseException('Lock is not owned by this thread') raise EventLock.LockReleaseException('Lock is not owned by this thread')

View File

@@ -258,7 +258,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
raise OrderError(err) raise OrderError(err)
@transaction.atomic() @transaction.atomic
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime, def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_provider: BasePaymentProvider, locale: str=None): payment_provider: BasePaymentProvider, locale: str=None):
total = sum([c.price for c in positions]) total = sum([c.price for c in positions])

View File

@@ -57,7 +57,7 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
context['sform'] = self.sform context['sform'] = self.sform
return context return context
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
self.sform.save() self.sform.save()
if self.sform.has_changed(): if self.sform.has_changed():
@@ -187,7 +187,7 @@ class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMi
context['providers'] = self.provider_forms context['providers'] = self.provider_forms
return self.render_to_response(context) return self.render_to_response(context)
@transaction.atomic() @transaction.atomic
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
success = self.sform.is_valid() success = self.sform.is_valid()
@@ -234,7 +234,7 @@ class EventSettingsFormView(EventPermissionRequiredMixin, FormView):
kwargs['obj'] = self.request.event kwargs['obj'] = self.request.event
return kwargs return kwargs
@transaction.atomic() @transaction.atomic
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
@@ -279,7 +279,7 @@ class DisplaySettings(EventSettingsFormView):
'event': self.request.event.slug 'event': self.request.event.slug
}) })
@transaction.atomic() @transaction.atomic
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
@@ -314,7 +314,7 @@ class MailSettings(EventSettingsFormView):
'event': self.request.event.slug 'event': self.request.event.slug
}) })
@transaction.atomic() @transaction.atomic
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
@@ -374,7 +374,7 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
form.prepare_fields() form.prepare_fields()
return form return form
@transaction.atomic() @transaction.atomic
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
success = True success = True
for provider in self.provider_forms: for provider in self.provider_forms:
@@ -470,7 +470,7 @@ class EventPermissions(EventPermissionRequiredMixin, TemplateView):
ctx['add_form'] = self.add_form ctx['add_form'] = self.add_form
return ctx return ctx
@transaction.atomic() @transaction.atomic
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
if self.formset.is_valid() and self.add_form.is_valid(): if self.formset.is_valid() and self.add_form.is_valid():
if self.add_form.has_changed(): if self.add_form.has_changed():

View File

@@ -99,7 +99,7 @@ class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
except ItemCategory.DoesNotExist: except ItemCategory.DoesNotExist:
raise Http404(_("The requested product category does not exist.")) raise Http404(_("The requested product category does not exist."))
@transaction.atomic() @transaction.atomic
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
for item in self.object.items.all(): for item in self.object.items.all():
@@ -134,7 +134,7 @@ class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
except ItemCategory.DoesNotExist: except ItemCategory.DoesNotExist:
raise Http404(_("The requested product category does not exist.")) raise Http404(_("The requested product category does not exist."))
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.')) messages.success(self.request, _('Your changes have been saved.'))
if form.has_changed(): if form.has_changed():
@@ -165,7 +165,7 @@ class CategoryCreate(EventPermissionRequiredMixin, CreateView):
'event': self.request.event.slug, 'event': self.request.event.slug,
}) })
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
form.instance.event = self.request.event form.instance.event = self.request.event
messages.success(self.request, _('The new category has been created.')) messages.success(self.request, _('The new category has been created.'))
@@ -299,7 +299,7 @@ class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
context['dependent'] = list(self.get_object().items.all()) context['dependent'] = list(self.get_object().items.all())
return context return context
@transaction.atomic() @transaction.atomic
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
success_url = self.get_success_url() success_url = self.get_success_url()
@@ -390,7 +390,7 @@ class QuestionUpdate(EventPermissionRequiredMixin, QuestionMixin, UpdateView):
except Question.DoesNotExist: except Question.DoesNotExist:
raise Http404(_("The requested question does not exist.")) raise Http404(_("The requested question does not exist."))
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
if form.cleaned_data.get('type') in ('M', 'C'): if form.cleaned_data.get('type') in ('M', 'C'):
if not self.save_formset(self.get_object()): if not self.save_formset(self.get_object()):
@@ -433,7 +433,7 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
def get_object(self, **kwargs): def get_object(self, **kwargs):
return None return None
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
if form.cleaned_data.get('type') in ('M', 'C'): if form.cleaned_data.get('type') in ('M', 'C'):
if not self.formset.is_valid(): if not self.formset.is_valid():
@@ -480,7 +480,7 @@ class QuotaEditorMixin:
item.field = self.get_form(QuotaForm)['item_%s' % item.id] item.field = self.get_form(QuotaForm)['item_%s' % item.id]
return context return context
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
res = super().form_valid(form) res = super().form_valid(form)
items = self.object.items.all() items = self.object.items.all()
@@ -516,7 +516,7 @@ class QuotaCreate(EventPermissionRequiredMixin, QuotaEditorMixin, CreateView):
'event': self.request.event.slug, 'event': self.request.event.slug,
}) })
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
form.instance.event = self.request.event form.instance.event = self.request.event
messages.success(self.request, _('The new quota has been created.')) messages.success(self.request, _('The new quota has been created.'))
@@ -540,7 +540,7 @@ class QuotaUpdate(EventPermissionRequiredMixin, QuotaEditorMixin, UpdateView):
except Quota.DoesNotExist: except Quota.DoesNotExist:
raise Http404(_("The requested quota does not exist.")) raise Http404(_("The requested quota does not exist."))
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.')) messages.success(self.request, _('Your changes have been saved.'))
if form.has_changed(): if form.has_changed():
@@ -577,7 +577,7 @@ class QuotaDelete(EventPermissionRequiredMixin, DeleteView):
context['dependent'] = list(self.get_object().items.all()) context['dependent'] = list(self.get_object().items.all())
return context return context
@transaction.atomic() @transaction.atomic
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
success_url = self.get_success_url() success_url = self.get_success_url()
@@ -621,7 +621,7 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
'item': self.object.id, 'item': self.object.id,
}) })
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.')) messages.success(self.request, _('Your changes have been saved.'))
ret = super().form_valid(form) ret = super().form_valid(form)
@@ -655,7 +655,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
'item': self.get_object().id, 'item': self.get_object().id,
}) })
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.')) messages.success(self.request, _('Your changes have been saved.'))
if form.has_changed(): if form.has_changed():

View File

@@ -126,7 +126,7 @@ class VoucherDelete(EventPermissionRequiredMixin, DeleteView):
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
@transaction.atomic() @transaction.atomic
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
success_url = self.get_success_url() success_url = self.get_success_url()
@@ -168,7 +168,7 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
except Voucher.DoesNotExist: except Voucher.DoesNotExist:
raise Http404(_("The requested voucher does not exist.")) raise Http404(_("The requested voucher does not exist."))
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.')) messages.success(self.request, _('Your changes have been saved.'))
if form.has_changed(): if form.has_changed():
@@ -210,7 +210,7 @@ class VoucherCreate(EventPermissionRequiredMixin, CreateView):
kwargs['instance'] = Voucher(event=self.request.event) kwargs['instance'] = Voucher(event=self.request.event)
return kwargs return kwargs
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
form.instance.event = self.request.event form.instance.event = self.request.event
messages.success(self.request, _('The new voucher has been created.')) messages.success(self.request, _('The new voucher has been created.'))
@@ -241,7 +241,7 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
kwargs['instance'] = Voucher(event=self.request.event) kwargs['instance'] = Voucher(event=self.request.event)
return kwargs return kwargs
@transaction.atomic() @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
for o in form.save(self.request.event): for o in form.save(self.request.event):
o.log_action('pretix.voucher.added', data=form.cleaned_data, user=self.request.user) o.log_action('pretix.voucher.added', data=form.cleaned_data, user=self.request.user)