Banktransfer: Added experimental HBCI support

This commit is contained in:
Raphael Michel
2015-08-20 21:53:10 +02:00
parent e2606cb456
commit b55b02f4b8
7 changed files with 227 additions and 13 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 \
build-essential python3-dev zlib1g-dev libssl-dev npm gettext git \
libpq-dev libmysqlclient-dev libmemcached-dev libjpeg-dev \
aqbanking-tools \
--no-install-recommends
WORKDIR /

View File

@@ -27,6 +27,10 @@ class BankTransferApp(AppConfig):
import chardet # NOQA
except ImportError:
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

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" %}
{% load i18n %}
{% load bootstrap3 %}
{% 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-heading">
<h3 class="panel-title">{% trans "Upload a new file" %}</h3>
</div>
<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">
{% csrf_token %}
<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 class="clearfix"></div>
<button class="btn btn-primary pull-right" type="submit">
<span class="icon icon-upload"></span> {% trans "Start upload" %}
</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>
{% endblock %}

View File

@@ -2,27 +2,48 @@ import csv
import json
import logging
import re
import shutil
import sys
from decimal import Decimal
from django import forms
from django.contrib import messages
from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import TemplateView
from pip.utils import cached_property
from pretix.base.models import Order, Quota
from pretix.base.services.orders import mark_order_paid
from pretix.base.settings import SettingsSandbox
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')
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):
template_name = 'pretixplugins/banktransfer/import_form.html'
permission = 'can_change_orders'
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()) \
or 'amount' in self.request.POST:
# Process CSV
@@ -59,9 +80,44 @@ class ImportView(EventPermissionRequiredMixin, TemplateView):
'contact support for help.'))
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):
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):
try:
data = csvimport.get_rows_from_file(self.request.FILES['file'])
@@ -89,10 +145,12 @@ class ImportView(EventPermissionRequiredMixin, TemplateView):
def process_csv_hint(self):
data = []
for i in range(int(self.request.POST.get('rows'))):
data.append([
self.request.POST.get('col[%d][%d]' % (i, j))
for j in range(int(self.request.POST.get('cols')))
])
data.append(
[
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:
messages.error(self.request, _('You need to select the column containing the payment reference.'))
return self.assign_view(data)
@@ -174,3 +232,7 @@ class ImportView(EventPermissionRequiredMixin, TemplateView):
row['class'] = 'warning'
row['message'] = _('Order has been refunded')
return data
@method_decorator(sensitive_post_parameters)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)

View File

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