Merge branch 'master' of github.com:pretix/pretix

This commit is contained in:
Raphael Michel
2015-08-21 11:37:35 +02:00
8 changed files with 227 additions and 22 deletions

View File

@@ -4,6 +4,7 @@ RUN apt-get update && apt-get install -y python3 git python3-pip \
libxml2-dev libxslt1-dev python-dev python-virtualenv locales libffi-dev \ libxml2-dev libxslt1-dev python-dev python-virtualenv locales libffi-dev \
build-essential python3-dev zlib1g-dev libssl-dev npm gettext git \ build-essential python3-dev zlib1g-dev libssl-dev npm gettext git \
libpq-dev libmysqlclient-dev libmemcached-dev libjpeg-dev \ libpq-dev libmysqlclient-dev libmemcached-dev libjpeg-dev \
aqbanking-tools \
--no-install-recommends --no-install-recommends
WORKDIR / WORKDIR /

View File

@@ -27,6 +27,10 @@ class BankTransferApp(AppConfig):
import chardet # NOQA import chardet # NOQA
except ImportError: except ImportError:
errs.append(_("Install the python package 'chardet' for better CSV import capabilities.")) errs.append(_("Install the python package 'chardet' for better CSV import capabilities."))
try:
import defusedxml # NOQA
except ImportError:
errs.append(_("Please install the python package 'defusedxml' for security reasons."))
return errs return errs

View File

@@ -0,0 +1,112 @@
import subprocess
import tempfile
import time
from decimal import Decimal
def hbci_transactions(event, conf):
try:
from defusedxml import ElementTree
except:
from xml.etree import ElementTree
log = []
data = []
accname = event.identity + '_' + str(int(time.time()))
try:
try:
subprocess.call([
'aqhbci-tool4', 'deluser', '-a', '--all',
'-b', conf['hbci_blz'],
'-u', conf['hbci_userid']
])
except subprocess.CalledProcessError:
pass
aqhbci_params = [
'aqhbci-tool4', 'adduser',
'-N', accname,
'-b', conf['hbci_blz'],
'-s', conf['hbci_server'],
'-t', conf['hbci_tokentype'],
'-u', conf['hbci_userid']
]
if conf['hbci_customerid']:
aqhbci_params += ['-c', conf['hbci_customerid']]
if conf['hbci_tokenname']:
aqhbci_params += ['-n', conf['hbci_tokenname']]
if conf['hbci_version']:
aqhbci_params += ['--hbciversion=' + str(conf['hbci_version'])]
aqhbci_add = subprocess.check_output(aqhbci_params)
log.append("$ " + " ".join(aqhbci_params))
log.append(aqhbci_add.decode("utf-8"))
with tempfile.NamedTemporaryFile() as f, tempfile.NamedTemporaryFile() as g:
f.write(('PIN_%s_%s = "%s"\n' % (
conf['hbci_blz'],
conf['hbci_userid'],
conf['pin'],
)).encode("utf-8"))
f.flush()
aqhbci_params = [
'aqhbci-tool4',
'-P', f.name,
'-n', '-A',
'getsysid'
]
aqhbci_test = subprocess.check_output(aqhbci_params)
log.append("$ " + " ".join(aqhbci_params))
log.append(aqhbci_test.decode("utf-8"))
aqbanking_params = [
'aqbanking-cli',
'-P', f.name, '-A', '-n',
'request',
'--transactions',
'-c', g.name
]
aqbanking_trans = subprocess.check_output(aqbanking_params)
log.append("$ " + " ".join(aqbanking_params))
log.append(aqbanking_trans.decode("utf-8"))
aqbanking_params = [
'aqbanking-cli',
'listtrans',
'-c', g.name,
'--exporter=xmldb',
]
aqbanking_conv = subprocess.check_output(aqbanking_params)
log.append("$ " + " ".join(aqbanking_params))
root = ElementTree.fromstring(aqbanking_conv)
trans_list = root.find('accountInfoList').find('accountInfo').find('transactionList')
for trans in trans_list.findall('transaction'):
payer = []
for child in trans:
if child.tag.startswith('remote'):
payer.append(child.find('value').text)
date = '%s-%02d-%02d' % (
trans.find('date').find('date').find('year').find('value').text,
int(trans.find('date').find('date').find('month').find('value').text),
int(trans.find('date').find('date').find('day').find('value').text)
)
value = trans.find('value').find('value').find('value').text
if "/" in value:
parts = value.split("/")
num = int(parts[0])
denom = int(parts[1])
value = Decimal(num) / Decimal(denom)
value = str(value.quantize(Decimal('.01')))
data.append({
'payer': "\n".join(payer),
'reference': trans.find('purpose').find('value').text,
'amount': value,
'date': date
})
except subprocess.CalledProcessError as e:
log.append("Command %s failed with %d and output:" % (e.cmd, e.returncode))
log.append(e.output.decode("utf-8"))
except Exception as e:
log.append(str(e))
finally:
subprocess.call([
'aqhbci-tool4', 'deluser', '-a', '-N', accname
])
log = "\n".join(log)
return data, log

View File

@@ -0,0 +1,9 @@
{% extends "pretixplugins/banktransfer/import_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<div class="alert alert-danger">
{% trans "An error occured during the HBCI transaction." %}
</div>
<pre>{{ log }}</pre>
{% endblock %}

View File

@@ -1,25 +1,50 @@
{% extends "pretixplugins/banktransfer/import_base.html" %} {% extends "pretixplugins/banktransfer/import_base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %}
{% block inner %} {% block inner %}
<p>{% blocktrans trimmed %}
This page allows you to upload bank statement files to process incoming payments.
{% endblocktrans %}</p>
<p>{% blocktrans trimmed %}
Currently, this feature supports <code>.csv</code> files and files in the MT940 format.
{% endblocktrans %}</p>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">{% trans "Upload a new file" %}</h3> <h3 class="panel-title">{% trans "Upload a new file" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p>{% blocktrans trimmed %}
This page allows you to upload bank statement files to process incoming payments.
{% endblocktrans %}</p>
<p>{% blocktrans trimmed %}
Currently, this feature supports <code>.csv</code> files and files in the MT940 format.
{% endblocktrans %}</p>
<form action="" method="post" enctype="multipart/form-data" class="form-inline"> <form action="" method="post" enctype="multipart/form-data" class="form-inline">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<input type="file" name="file" /> <label for="file">{% trans "Import file" %}: </label> <input id="file" type="file" name="file" />
</div> </div>
<div class="clearfix"></div>
<button class="btn btn-primary pull-right" type="submit"> <button class="btn btn-primary pull-right" type="submit">
<span class="icon icon-upload"></span> {% trans "Start upload" %} <span class="icon icon-upload"></span> {% trans "Start upload" %}
</button> </button>
</form></div> </form>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "HBCI import" %}</h3>
</div>
<div class="panel-body">
{% if hbci_available %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form hbci_form layout='horizontal' %}
<div class="clearfix"></div>
{% trans "Please note that this step might take a few minutes." %}
<button class="btn btn-primary pull-right" type="submit">
<span class="icon icon-upload"></span> {% trans "Import" %}
</button>
</form>
{% else %}
<div class="alert alert-error">
{% trans "HBCI is only available with Python 3.3 or newer and with aqbanking-cli and aqhbci-tool4 installed." %}
</div>
{% endif %}
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -2,27 +2,46 @@ import csv
import json import json
import logging import logging
import re import re
import shutil
import sys
from decimal import Decimal from decimal import Decimal
from django import forms
from django.contrib import messages from django.contrib import messages
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.utils.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView from django.views.generic import TemplateView
from pretix.base.models import Order, Quota from pretix.base.models import Order, Quota
from pretix.base.services.orders import mark_order_paid from pretix.base.services.orders import mark_order_paid
from pretix.base.settings import SettingsSandbox
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.plugins.banktransfer import csvimport, mt940import from pretix.plugins.banktransfer import csvimport, hbci, mt940import
logger = logging.getLogger('pretix.plugins.banktransfer') logger = logging.getLogger('pretix.plugins.banktransfer')
class HbciForm(forms.Form):
hbci_blz = forms.CharField(label=_("Bank code"))
hbci_userid = forms.CharField(label=_("User ID"))
hbci_customerid = forms.CharField(label=_("Customer ID"), required=False)
hbci_tokentype = forms.CharField(label=_("Token type"), initial='pintan')
hbci_tokenname = forms.CharField(label=_("Token name"), required=False)
hbci_server = forms.URLField(label=_("Server URL"))
hbci_version = forms.IntegerField(label=_("HBCI version"), required=False, initial=220)
pin = forms.CharField(label=_("PIN"), widget=forms.PasswordInput)
class ImportView(EventPermissionRequiredMixin, TemplateView): class ImportView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixplugins/banktransfer/import_form.html' template_name = 'pretixplugins/banktransfer/import_form.html'
permission = 'can_change_orders' permission = 'can_change_orders'
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
if 'hbci_server' in self.request.POST:
return self.process_hbci()
if ('file' in self.request.FILES and 'csv' in self.request.FILES.get('file').name.lower()) \ if ('file' in self.request.FILES and 'csv' in self.request.FILES.get('file').name.lower()) \
or 'amount' in self.request.POST: or 'amount' in self.request.POST:
# Process CSV # Process CSV
@@ -59,9 +78,44 @@ class ImportView(EventPermissionRequiredMixin, TemplateView):
'contact support for help.')) 'contact support for help.'))
return self.redirect_back() return self.redirect_back()
@cached_property
def settings(self):
return SettingsSandbox('payment', 'banktransfer', self.request.event)
def process_hbci(self):
form = HbciForm(data=self.request.POST if self.request.method == "POST" else None,
initial=self.settings)
if form.is_valid():
for key, value in form.cleaned_data.items():
if key.startswith('hbci_'):
self.settings.set(key, value)
data, log = hbci.hbci_transactions(self.request.event, form.cleaned_data)
if data:
return self.confirm_view(data)
return render(self.request, 'pretixplugins/banktransfer/hbci_log.html', {
'log': log
})
else:
return self.get(*self.args, **self.kwargs)
def process_mt940(self): def process_mt940(self):
return self.confirm_view(mt940import.parse(self.request.FILES.get('file'))) return self.confirm_view(mt940import.parse(self.request.FILES.get('file')))
@cached_property
def hbci_form(self):
return HbciForm(data=self.request.POST if self.request.method == "POST" else None,
initial=self.settings)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
if sys.version_info[:2] >= (3, 3):
ctx['hbci_available'] = shutil.which('aqbanking-cli') and shutil.which('aqhbci-tool4')
if ctx['hbci_available']:
ctx['hbci_form'] = self.hbci_form
else:
ctx['hbci_available'] = False
return ctx
def process_csv_file(self): def process_csv_file(self):
try: try:
data = csvimport.get_rows_from_file(self.request.FILES['file']) data = csvimport.get_rows_from_file(self.request.FILES['file'])
@@ -89,10 +143,12 @@ class ImportView(EventPermissionRequiredMixin, TemplateView):
def process_csv_hint(self): def process_csv_hint(self):
data = [] data = []
for i in range(int(self.request.POST.get('rows'))): for i in range(int(self.request.POST.get('rows'))):
data.append([ data.append(
self.request.POST.get('col[%d][%d]' % (i, j)) [
for j in range(int(self.request.POST.get('cols'))) self.request.POST.get('col[%d][%d]' % (i, j))
]) for j in range(int(self.request.POST.get('cols')))
]
)
if 'reference' not in self.request.POST: if 'reference' not in self.request.POST:
messages.error(self.request, _('You need to select the column containing the payment reference.')) messages.error(self.request, _('You need to select the column containing the payment reference.'))
return self.assign_view(data) return self.assign_view(data)
@@ -141,7 +197,7 @@ class ImportView(EventPermissionRequiredMixin, TemplateView):
row['ok'] = False row['ok'] = False
match = pattern.search(row['reference'].upper()) match = pattern.search(row['reference'].upper())
if not match: if not match:
row['class'] = '' row['class'] = 'warning'
row['message'] = _('No order code detected') row['message'] = _('No order code detected')
continue continue
@@ -150,24 +206,23 @@ class ImportView(EventPermissionRequiredMixin, TemplateView):
order = Order.objects.current.get(event=self.request.event, order = Order.objects.current.get(event=self.request.event,
code=code) code=code)
except Order.DoesNotExist: except Order.DoesNotExist:
row['class'] = 'error' row['class'] = 'danger'
row['message'] = _('Unknown order code detected') row['message'] = _('Unknown order code detected')
else: else:
row['order'] = order row['order'] = order
if order.status == Order.STATUS_PENDING: if order.status == Order.STATUS_PENDING:
amount = Decimal(amount_pattern.sub("", row['amount'].replace(",", "."))) amount = Decimal(amount_pattern.sub("", row['amount'].replace(",", ".")))
if amount != order.total: if amount != order.total:
row['class'] = 'error' row['class'] = 'danger'
row['message'] = _('Found wrong amount. Expected: %s' % str(order.total)) row['message'] = _('Found wrong amount. Expected: %s' % str(order.total))
else: else:
row['class'] = 'success' row['class'] = 'success'
row['message'] = _('Valid payment') row['message'] = _('Valid payment')
row['ok'] = True row['ok'] = True
elif order.status == Order.STATUS_CANCELLED: elif order.status == Order.STATUS_CANCELLED:
row['class'] = 'error' row['class'] = 'danger'
row['message'] = _('Order has been cancelled') row['message'] = _('Order has been cancelled')
elif order.status == Order.STATUS_PAID: elif order.status == Order.STATUS_PAID:
# TODO: Do a plausibility check to tell duplicate payments from overlapping import files
row['class'] = '' row['class'] = ''
row['message'] = _('Order already has been paid') row['message'] = _('Order already has been paid')
elif order.status == Order.STATUS_REFUNDED: elif order.status == Order.STATUS_REFUNDED:

View File

@@ -1,4 +1,4 @@
import importlib import importlib.util
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
@@ -22,13 +22,11 @@ if settings.DEBUG:
pluginpatterns = [] pluginpatterns = []
for app in apps.get_app_configs(): for app in apps.get_app_configs():
if hasattr(app, 'PretixPluginMeta'): if hasattr(app, 'PretixPluginMeta'):
try: if importlib.util.find_spec(app.name + '.urls'):
urlmod = importlib.import_module(app.name + '.urls') urlmod = importlib.import_module(app.name + '.urls')
pluginpatterns.append( pluginpatterns.append(
url(r'', include(urlmod, namespace=app.label)) url(r'', include(urlmod, namespace=app.label))
) )
except ImportError:
pass
urlpatterns.append( urlpatterns.append(
url(r'', include(pluginpatterns, namespace='plugins')) url(r'', include(pluginpatterns, namespace='plugins'))
) )

View File

@@ -1,2 +1,3 @@
chardet>=2.3,<3 chardet>=2.3,<3
defusedxml