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 \
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,46 @@ 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.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView
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 +78,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 +143,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)
@@ -141,7 +197,7 @@ class ImportView(EventPermissionRequiredMixin, TemplateView):
row['ok'] = False
match = pattern.search(row['reference'].upper())
if not match:
row['class'] = ''
row['class'] = 'warning'
row['message'] = _('No order code detected')
continue
@@ -150,24 +206,23 @@ class ImportView(EventPermissionRequiredMixin, TemplateView):
order = Order.objects.current.get(event=self.request.event,
code=code)
except Order.DoesNotExist:
row['class'] = 'error'
row['class'] = 'danger'
row['message'] = _('Unknown order code detected')
else:
row['order'] = order
if order.status == Order.STATUS_PENDING:
amount = Decimal(amount_pattern.sub("", row['amount'].replace(",", ".")))
if amount != order.total:
row['class'] = 'error'
row['class'] = 'danger'
row['message'] = _('Found wrong amount. Expected: %s' % str(order.total))
else:
row['class'] = 'success'
row['message'] = _('Valid payment')
row['ok'] = True
elif order.status == Order.STATUS_CANCELLED:
row['class'] = 'error'
row['class'] = 'danger'
row['message'] = _('Order has been cancelled')
elif order.status == Order.STATUS_PAID:
# TODO: Do a plausibility check to tell duplicate payments from overlapping import files
row['class'] = ''
row['message'] = _('Order already has been paid')
elif order.status == Order.STATUS_REFUNDED:

View File

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

View File

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