mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Merge branch 'master' of github.com:pretix/pretix
This commit is contained in:
@@ -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 /
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
112
src/pretix/plugins/banktransfer/hbci.py
Normal file
112
src/pretix/plugins/banktransfer/hbci.py
Normal 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
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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'))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
chardet>=2.3,<3
|
chardet>=2.3,<3
|
||||||
|
defusedxml
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user