Organizer-level bank import

This commit is contained in:
Raphael Michel
2017-06-27 12:31:45 +02:00
parent ea2557274f
commit d446191cf4
14 changed files with 444 additions and 94 deletions
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-06-19 11:25
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0062_auto_20170602_0948'),
('banktransfer', '0003_banktransaction_comment'),
]
operations = [
migrations.AddField(
model_name='bankimportjob',
name='organizer',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Organizer'),
),
migrations.AddField(
model_name='banktransaction',
name='organizer',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Organizer'),
),
migrations.AlterField(
model_name='bankimportjob',
name='event',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event'),
),
migrations.AlterField(
model_name='banktransaction',
name='event',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Event'),
),
migrations.AlterUniqueTogether(
name='banktransaction',
unique_together=set([('event', 'organizer', 'checksum')]),
),
]
+12 -3
View File
@@ -16,10 +16,18 @@ class BankImportJob(models.Model):
(STATE_COMPLETED, 'completed'), (STATE_COMPLETED, 'completed'),
) )
event = models.ForeignKey('pretixbase.Event') event = models.ForeignKey('pretixbase.Event', null=True)
organizer = models.ForeignKey('pretixbase.Organizer', null=True)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
state = models.CharField(max_length=32, choices=STATES, default=STATE_PENDING) state = models.CharField(max_length=32, choices=STATES, default=STATE_PENDING)
@property
def owner_kwargs(self):
if self.event:
return {'event': self.event}
else:
return {'organizer': self.organizer}
class BankTransaction(models.Model): class BankTransaction(models.Model):
STATE_UNCHECKED = 'imported' STATE_UNCHECKED = 'imported'
@@ -40,7 +48,8 @@ class BankTransaction(models.Model):
(STATE_DISCARDED, 'manually discarded'), (STATE_DISCARDED, 'manually discarded'),
) )
event = models.ForeignKey('pretixbase.Event') event = models.ForeignKey('pretixbase.Event', null=True)
organizer = models.ForeignKey('pretixbase.Organizer', null=True)
import_job = models.ForeignKey('BankImportJob', related_name='transactions') import_job = models.ForeignKey('BankImportJob', related_name='transactions')
state = models.CharField(max_length=32, choices=STATES, default=STATE_UNCHECKED) state = models.CharField(max_length=32, choices=STATES, default=STATE_UNCHECKED)
message = models.TextField() message = models.TextField()
@@ -66,4 +75,4 @@ class BankTransaction(models.Model):
self.reference = "" self.reference = ""
class Meta: class Meta:
unique_together = ('event', 'checksum') unique_together = ('event', 'organizer', 'checksum')
+20 -1
View File
@@ -4,7 +4,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.signals import register_payment_providers from pretix.base.signals import register_payment_providers
from pretix.control.signals import html_head, nav_event from pretix.control.signals import html_head, nav_event, nav_organizer
from .payment import BankTransfer from .payment import BankTransfer
@@ -32,6 +32,25 @@ def control_nav_import(sender, request=None, **kwargs):
] ]
@receiver(nav_organizer, dispatch_uid="payment_banktransfer_organav")
def control_nav_orga_import(sender, request=None, **kwargs):
url = resolve(request.path_info)
if not request.user.has_organizer_permission(request.organizer, 'can_change_orders'):
return []
if not request.organizer.events.filter(plugins__icontains='pretix.plugins.banktransfer'):
return []
return [
{
'label': _('Import bank data'),
'url': reverse('plugins:banktransfer:import', kwargs={
'organizer': request.organizer.slug,
}),
'active': (url.namespace == 'plugins:banktransfer' and url.url_name == 'import'),
'icon': 'upload',
}
]
@receiver(html_head, dispatch_uid="banktransfer_html_head") @receiver(html_head, dispatch_uid="banktransfer_html_head")
def html_head_presale(sender, request=None, **kwargs): def html_head_presale(sender, request=None, **kwargs):
url = resolve(request.path_info) url = resolve(request.path_info)
+52 -21
View File
@@ -6,10 +6,11 @@ from decimal import Decimal
from celery.exceptions import MaxRetriesExceededError from celery.exceptions import MaxRetriesExceededError
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
from django.db.models import Q
from django.utils.translation import ugettext_noop from django.utils.translation import ugettext_noop
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Event, Order, Quota from pretix.base.models import Event, Order, Organizer, Quota
from pretix.base.services.async import TransactionAwareTask from pretix.base.services.async import TransactionAwareTask
from pretix.base.services.locking import LockTimeoutException from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException from pretix.base.services.mail import SendMailException
@@ -21,17 +22,33 @@ from .models import BankImportJob, BankTransaction
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _handle_transaction(event: Event, trans: BankTransaction, code: str): def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, organizer: Organizer=None,
try: slug: str=None):
trans.order = event.orders.get(code=code) if event:
except Order.DoesNotExist:
normalized_code = Order.normalize_code(code)
try: try:
trans.order = event.orders.get(code=normalized_code) trans.order = event.orders.get(code=code)
except Order.DoesNotExist: except Order.DoesNotExist:
trans.state = BankTransaction.STATE_NOMATCH normalized_code = Order.normalize_code(code)
trans.save() try:
return trans.order = event.orders.get(code=normalized_code)
except Order.DoesNotExist:
trans.state = BankTransaction.STATE_NOMATCH
trans.save()
return
else:
qs = Order.objects.filter(event__organizer=organizer)
if slug:
qs = qs.filter(event__slug__iexact=slug)
try:
trans.order = qs.get(code=code)
except Order.DoesNotExist:
normalized_code = Order.normalize_code(code)
try:
trans.order = qs.get(code=normalized_code)
except Order.DoesNotExist:
trans.state = BankTransaction.STATE_NOMATCH
trans.save()
return
if trans.order.status == Order.STATUS_PAID: if trans.order.status == Order.STATUS_PAID:
trans.state = BankTransaction.STATE_DUPLICATE trans.state = BankTransaction.STATE_DUPLICATE
@@ -63,9 +80,11 @@ def _handle_transaction(event: Event, trans: BankTransaction, code: str):
trans.save() trans.save()
def _get_unknown_transactions(event: Event, job: BankImportJob, data: list): def _get_unknown_transactions(job: BankImportJob, data: list, event: Event=None, organizer: Organizer=None):
amount_pattern = re.compile("[^0-9.-]") amount_pattern = re.compile("[^0-9.-]")
known_checksums = set(t['checksum'] for t in BankTransaction.objects.filter(event=event).values('checksum')) known_checksums = set(t['checksum'] for t in BankTransaction.objects.filter(
Q(event=event) if event else Q(organizer=organizer)
).values('checksum'))
transactions = [] transactions = []
for row in data: for row in data:
@@ -83,7 +102,7 @@ def _get_unknown_transactions(event: Event, job: BankImportJob, data: list):
logger.exception('Could not parse amount of transaction: {}'.format(amount)) logger.exception('Could not parse amount of transaction: {}'.format(amount))
amount = Decimal("0.00") amount = Decimal("0.00")
trans = BankTransaction(event=event, import_job=job, trans = BankTransaction(event=event, organizer=organizer, import_job=job,
payer=row.get('payer', ''), payer=row.get('payer', ''),
reference=row['reference'], reference=row['reference'],
amount=amount, amount=amount,
@@ -99,29 +118,41 @@ def _get_unknown_transactions(event: Event, job: BankImportJob, data: list):
@app.task(base=TransactionAwareTask, bind=True, max_retries=5, default_retry_delay=1) @app.task(base=TransactionAwareTask, bind=True, max_retries=5, default_retry_delay=1)
def process_banktransfers(self, event: int, job: int, data: list) -> None: def process_banktransfers(self, job: int, data: list) -> None:
with language("en"): # We'll translate error messages at display time with language("en"): # We'll translate error messages at display time
event = Event.objects.get(pk=event)
job = BankImportJob.objects.get(pk=job) job = BankImportJob.objects.get(pk=job)
job.state = BankImportJob.STATE_RUNNING job.state = BankImportJob.STATE_RUNNING
job.save() job.save()
prefixes = []
try: try:
# Delete left-over transactions from a failed run before so they can reimported # Delete left-over transactions from a failed run before so they can reimported
BankTransaction.objects.filter(event=event, state=BankTransaction.STATE_UNCHECKED).delete() BankTransaction.objects.filter(state=BankTransaction.STATE_UNCHECKED, **job.owner_kwargs).delete()
transactions = _get_unknown_transactions(event, job, data) transactions = _get_unknown_transactions(job, data, **job.owner_kwargs)
code_len = settings.ENTROPY['order_code'] code_len = settings.ENTROPY['order_code']
pattern = re.compile(event.slug.upper() + "[ \-_]*([A-Z0-9]{%s})" % code_len) if job.event:
pattern = re.compile(job.event.slug.upper() + "[ \-_]*([A-Z0-9]{%s})" % code_len)
else:
if not prefixes:
prefixes = [e.slug.upper().replace(".", r"\.").replace("-", r"\-")
for e in job.organizer.events.all()]
pattern = re.compile("(%s)[ \-_]*([A-Z0-9]{%s})" % ("|".join(prefixes), code_len))
for trans in transactions: for trans in transactions:
match = pattern.search(trans.reference.upper()) match = pattern.search(trans.reference.upper())
if match: if match:
code = match.group(1) if job.event:
with transaction.atomic(): code = match.group(1)
_handle_transaction(event, trans, code) with transaction.atomic():
_handle_transaction(trans, code, event=job.event)
else:
slug = match.group(1)
code = match.group(2)
with transaction.atomic():
_handle_transaction(trans, code, organizer=job.organizer, slug=slug)
else: else:
trans.state = BankTransaction.STATE_NOMATCH trans.state = BankTransaction.STATE_NOMATCH
trans.save() trans.save()
@@ -1,4 +1,4 @@
{% extends "pretixplugins/banktransfer/import_base.html" %} {% extends basetpl %}
{% load i18n %} {% load i18n %}
{% block inner %} {% block inner %}
<p>{% blocktrans trimmed %} <p>{% blocktrans trimmed %}
@@ -6,5 +6,4 @@
<h1>{% trans "Import bank data" %}</h1> <h1>{% trans "Import bank data" %}</h1>
{% block inner %} {% block inner %}
{% endblock %} {% endblock %}
<script type="application/javascript" src="{% static "pretixplugins/banktransfer/ui.js" %}"></script>
{% endblock %} {% endblock %}
@@ -0,0 +1,4 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Import bank data" %}{% endblock %}
@@ -1,6 +1,7 @@
{% extends "pretixplugins/banktransfer/import_base.html" %} {% extends basetpl %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load static %}
{% block inner %} {% block inner %}
{% if no_more_payments %} {% if no_more_payments %}
<div class="alert alert-danger"> <div class="alert alert-danger">
@@ -47,6 +48,16 @@
<h3 class="panel-title">{% trans "Unresolved transactions" %}</h3> <h3 class="panel-title">{% trans "Unresolved transactions" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% if request.event %}
<p>
{% blocktrans %}
On this page, you can import banking data on a per-event level. You also only see
unmatched transactions imported directly for this event.
{% endblocktrans %}
<a href="{% url "plugins:banktransfer:import" organizer=request.organizer.slug %}"
class="btn btn-default btn-xs">{% trans "Go to organizer-level import" %}</a>
</p>
{% endif %}
<p> <p>
<form class="form-inline helper-display-inline" action="" method="get"> <form class="form-inline helper-display-inline" action="" method="get">
<input type="text" name="search" class="form-control" placeholder="{% trans "Search" %}" <input type="text" name="search" class="form-control" placeholder="{% trans "Search" %}"
@@ -74,4 +85,5 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<script type="application/javascript" src="{% static "pretixplugins/banktransfer/ui.js" %}"></script>
{% endblock %} {% endblock %}
@@ -1,5 +1,6 @@
{% extends "pretixplugins/banktransfer/import_base.html" %} {% extends basetpl %}
{% load i18n %} {% load i18n %}
{% load staticfiles %}
{% block inner %} {% block inner %}
<h2>{% trans "Import result" %}</h2> <h2>{% trans "Import result" %}</h2>
{% if job.state == "running" or job.state == "pending" %} {% if job.state == "running" or job.state == "pending" %}
@@ -37,11 +38,12 @@
</tr> </tr>
</table> </table>
{% if transactions_ignored or transactions_invalid %} {% if transactions_ignored or transactions_invalid %}
<a href="{% url "plugins:banktransfer:import" event=request.event.slug organizer=request.organizer.slug %}" <a href="{% if request.event %}{% url "plugins:banktransfer:import" event=request.event.slug organizer=request.organizer.slug %}{% else %}{% url "plugins:banktransfer:import" organizer=request.organizer.slug %}{% endif %}"
class="btn btn-primary"> class="btn btn-primary">
{% trans "Review invalid and ignored payments" %} &raquo; {% trans "Review invalid and ignored payments" %} &raquo;
</a> </a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<script type="application/javascript" src="{% static "pretixplugins/banktransfer/ui.js" %}"></script>
{% endblock %} {% endblock %}
@@ -2,7 +2,7 @@
{% load staticfiles %} {% load staticfiles %}
<div class="table-responsive"> <div class="table-responsive">
{% csrf_token %} {% csrf_token %}
<table class="table table-condensed transaction-list" data-url="{% url "plugins:banktransfer:import.action" event=request.event.slug organizer=request.event.organizer.slug %}"> <table class="table table-condensed transaction-list" data-url="{% if request.event %}{% url "plugins:banktransfer:import.action" event=request.event.slug organizer=request.organizer.slug %}{% else %}{% url "plugins:banktransfer:import.action" organizer=request.organizer.slug %}{% endif %}">
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
@@ -83,9 +83,13 @@
</td> </td>
<td> <td>
{% if trans.order %} {% if trans.order %}
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=trans.order.code %}" <a href="{% url "control:event.order" event=trans.order.event.slug organizer=request.organizer.slug code=trans.order.code %}"
data-toggle="tooltip" title="{{ trans.order.total|floatformat:2 }} {{ request.event.currency }}"> data-toggle="tooltip" title="{{ trans.order.total|floatformat:2 }} {{ trans.order.event.currency }}">
{{ trans.order.code }} {% if not request.event %}
{{ trans.order.event.slug|upper }}-{{ trans.order.code }}
{% else %}
{{ trans.order.code }}
{% endif %}
</a> </a>
{% endif %} {% endif %}
</td> </td>
+12 -3
View File
@@ -3,10 +3,19 @@ from django.conf.urls import url
from . import views from . import views
urlpatterns = [ urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/banktransfer/import/', views.ImportView.as_view(), url(r'^control/organizer/(?P<organizer>[^/]+)/banktransfer/import/',
views.OrganizerImportView.as_view(),
name='import'),
url(r'^control/organizer/(?P<organizer>[^/]+)/banktransfer/job/(?P<job>\d+)/',
views.OrganizerJobDetailView.as_view(), name='import.job'),
url(r'^control/organizer/(?P<organizer>[^/]+)/banktransfer/action/',
views.OrganizerActionView.as_view(), name='import.action'),
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/banktransfer/import/',
views.EventImportView.as_view(),
name='import'), name='import'),
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/banktransfer/job/(?P<job>\d+)/', url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/banktransfer/job/(?P<job>\d+)/',
views.JobDetailView.as_view(), name='import.job'), views.EventJobDetailView.as_view(), name='import.job'),
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/banktransfer/action/', url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/banktransfer/action/',
views.ActionView.as_view(), name='import.action'), views.EventActionView.as_view(), name='import.action'),
] ]
+160 -52
View File
@@ -17,7 +17,10 @@ from pretix.base.models import Order, Quota
from pretix.base.services.mail import SendMailException from pretix.base.services.mail import SendMailException
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.base.settings import SettingsSandbox
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import (
EventPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
)
from pretix.control.views.organizer import OrganizerDetailViewMixin
from pretix.plugins.banktransfer import csvimport, mt940import from pretix.plugins.banktransfer import csvimport, mt940import
from pretix.plugins.banktransfer.models import BankImportJob, BankTransaction from pretix.plugins.banktransfer.models import BankImportJob, BankTransaction
from pretix.plugins.banktransfer.tasks import process_banktransfers from pretix.plugins.banktransfer.tasks import process_banktransfers
@@ -25,7 +28,7 @@ from pretix.plugins.banktransfer.tasks import process_banktransfers
logger = logging.getLogger('pretix.plugins.banktransfer') logger = logging.getLogger('pretix.plugins.banktransfer')
class ActionView(EventPermissionRequiredMixin, View): class ActionView(View):
permission = 'can_change_orders' permission = 'can_change_orders'
def _discard(self, trans): def _discard(self, trans):
@@ -89,7 +92,10 @@ class ActionView(EventPermissionRequiredMixin, View):
def _assign(self, trans, code): def _assign(self, trans, code):
try: try:
trans.order = self.request.event.orders.get(code=code) if '-' in code:
trans.order = self.order_qs().get(code=code.split('-')[1], event__slug__iexact=code.split('-')[0])
else:
trans.order = self.order_qs().get(code=code.split('-')[-1])
except Order.DoesNotExist: except Order.DoesNotExist:
return JsonResponse({ return JsonResponse({
'status': 'error', 'status': 'error',
@@ -109,7 +115,10 @@ class ActionView(EventPermissionRequiredMixin, View):
for k, v in request.POST.items(): for k, v in request.POST.items():
if not k.startswith('action_'): if not k.startswith('action_'):
continue continue
trans = get_object_or_404(BankTransaction, id=k.split('_')[1], event=self.request.event) if 'event' in kwargs:
trans = get_object_or_404(BankTransaction, id=k.split('_')[1], event=request.event)
else:
trans = get_object_or_404(BankTransaction, id=k.split('_')[1], organizer=request.organizer)
if v == 'discard' and trans.state in (BankTransaction.STATE_INVALID, BankTransaction.STATE_ERROR, if v == 'discard' and trans.state in (BankTransaction.STATE_INVALID, BankTransaction.STATE_ERROR,
BankTransaction.STATE_NOMATCH, BankTransaction.STATE_DUPLICATE): BankTransaction.STATE_NOMATCH, BankTransaction.STATE_DUPLICATE):
@@ -140,52 +149,64 @@ class ActionView(EventPermissionRequiredMixin, View):
if len(query) < 2: if len(query) < 2:
return JsonResponse({'results': []}) return JsonResponse({'results': []})
qs = self.request.event.orders.filter(Q(code__icontains=query) | Q(code__icontains=Order.normalize_code(query))) qs = self.order_qs().filter(Q(code__icontains=query) | Q(code__icontains=Order.normalize_code(query))).select_related('event')
return JsonResponse({ return JsonResponse({
'results': [ 'results': [
{ {
'code': o.code, 'code': o.event.slug.upper() + '-' + o.code,
'status': o.get_status_display(), 'status': o.get_status_display(),
'total': localize(o.total) + ' ' + self.request.event.currency 'total': localize(o.total) + ' ' + o.event.currency
} for o in qs } for o in qs
] ]
}) })
def order_qs(self):
return self.request.event.orders
class JobDetailView(EventPermissionRequiredMixin, DetailView):
class JobDetailView(DetailView):
template_name = 'pretixplugins/banktransfer/job_detail.html' template_name = 'pretixplugins/banktransfer/job_detail.html'
permission = 'can_change_orders' permission = 'can_change_orders'
context_objectname = 'job' context_objectname = 'job'
def redirect_form(self): def redirect_form(self):
return redirect(reverse('plugins:banktransfer:import', kwargs={ kwargs = {
'event': self.request.event.slug, 'organizer': self.request.organizer.slug,
'organizer': self.request.event.organizer.slug, }
})) if 'event' in self.kwargs:
kwargs['event'] = self.kwargs['event']
return redirect(reverse('plugins:banktransfer:import', kwargs=kwargs))
def redirect_back(self): def redirect_back(self):
return redirect(reverse('plugins:banktransfer:import.job', kwargs={ kwargs = {
'event': self.request.event.slug, 'organizer': self.request.organizer.slug,
'organizer': self.request.event.organizer.slug,
'job': self.kwargs['job'] 'job': self.kwargs['job']
})) }
if 'event' in self.kwargs:
kwargs['event'] = self.kwargs['event']
return redirect(reverse('plugins:banktransfer:import.job', kwargs=kwargs))
def get_object(self, queryset=None): @cached_property
return get_object_or_404(BankImportJob, id=self.kwargs['job'], event=self.request.event) def job(self):
if 'event' in self.kwargs:
kwargs = {'event': self.request.event}
else:
kwargs = {'organizer': self.request.organizer}
return get_object_or_404(BankImportJob, id=self.kwargs['job'], **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if 'ajax' in request.GET: if 'ajax' in request.GET:
self.object = self.get_object()
return JsonResponse({ return JsonResponse({
'state': self.object.state 'state': self.job.state
}) })
return super().get(request, *args, **kwargs) context = self.get_context_data()
return self.render_to_response(context)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data() ctx = {}
qs = self.object.transactions.select_related('order') qs = self.job.transactions.select_related('order', 'order__event')
ctx['transactions_valid'] = qs.filter(state=BankTransaction.STATE_VALID).count() ctx['transactions_valid'] = qs.filter(state=BankTransaction.STATE_VALID).count()
ctx['transactions_invalid'] = qs.filter(state__in=[ ctx['transactions_invalid'] = qs.filter(state__in=[
@@ -194,21 +215,33 @@ class JobDetailView(EventPermissionRequiredMixin, DetailView):
ctx['transactions_ignored'] = qs.filter(state__in=[ ctx['transactions_ignored'] = qs.filter(state__in=[
BankTransaction.STATE_DUPLICATE, BankTransaction.STATE_NOMATCH BankTransaction.STATE_DUPLICATE, BankTransaction.STATE_NOMATCH
]).count() ]).count()
ctx['job'] = self.object ctx['job'] = self.job
ctx['organizer'] = self.request.organizer
if 'event' in self.kwargs:
ctx['basetpl'] = 'pretixplugins/banktransfer/import_base.html'
else:
ctx['basetpl'] = 'pretixplugins/banktransfer/import_base_organizer.html'
return ctx return ctx
class ImportView(EventPermissionRequiredMixin, ListView): class ImportView(ListView):
template_name = 'pretixplugins/banktransfer/import_form.html' template_name = 'pretixplugins/banktransfer/import_form.html'
permission = 'can_change_orders' permission = 'can_change_orders'
context_object_name = 'transactions_unhandled' context_object_name = 'transactions_unhandled'
paginate_by = 30 paginate_by = 30
def get_queryset(self): def get_queryset(self):
qs = BankTransaction.objects.filter( if 'event' in self.kwargs:
event=self.request.event qs = BankTransaction.objects.filter(
).select_related('order').filter(state__in=[ Q(event=self.request.event)
)
else:
qs = BankTransaction.objects.filter(
Q(organizer=self.request.organizer)
)
qs = qs.select_related('order').filter(state__in=[
BankTransaction.STATE_INVALID, BankTransaction.STATE_ERROR, BankTransaction.STATE_INVALID, BankTransaction.STATE_ERROR,
BankTransaction.STATE_DUPLICATE, BankTransaction.STATE_NOMATCH BankTransaction.STATE_DUPLICATE, BankTransaction.STATE_NOMATCH
]) ])
@@ -245,12 +278,12 @@ class ImportView(EventPermissionRequiredMixin, ListView):
else: else:
messages.error(self.request, _('We were unable to detect the file type of this import. Please ' messages.error(self.request, _('We were unable to detect the file type of this import. Please '
'contact support for help.')) 'contact support for help.'))
return self.redirect_back() return self.redirect_back()
@cached_property @cached_property
def settings(self): def settings(self):
return SettingsSandbox('payment', 'banktransfer', self.request.event) return SettingsSandbox('payment', 'banktransfer', getattr(self.request, 'event', self.request.organizer))
def process_mt940(self): def process_mt940(self):
try: try:
@@ -261,6 +294,7 @@ class ImportView(EventPermissionRequiredMixin, ListView):
return self.redirect_back() return self.redirect_back()
def process_csv_file(self): def process_csv_file(self):
o = getattr(self.request, 'event', self.request.organizer)
try: try:
data = csvimport.get_rows_from_file(self.request.FILES['file']) data = csvimport.get_rows_from_file(self.request.FILES['file'])
except csv.Error as e: # TODO: narrow down except csv.Error as e: # TODO: narrow down
@@ -273,8 +307,8 @@ class ImportView(EventPermissionRequiredMixin, ListView):
messages.error(self.request, _('I\'m sorry, but we detected this file as empty. Please ' messages.error(self.request, _('I\'m sorry, but we detected this file as empty. Please '
'contact support for help.')) 'contact support for help.'))
if self.request.event.settings.get('banktransfer_csvhint') is not None: if o.settings.get('banktransfer_csvhint') is not None:
hint = self.request.event.settings.get('banktransfer_csvhint', as_type=dict) hint = o.settings.get('banktransfer_csvhint', as_type=dict)
try: try:
parsed, good = csvimport.parse(data, hint) parsed, good = csvimport.parse(data, hint)
@@ -304,8 +338,9 @@ class ImportView(EventPermissionRequiredMixin, ListView):
logger.error('Parsing hint failed: ' + str(e)) logger.error('Parsing hint failed: ' + str(e))
messages.error(self.request, _('We were unable to process your input.')) messages.error(self.request, _('We were unable to process your input.'))
return self.assign_view(data) return self.assign_view(data)
o = getattr(self.request, 'event', self.request.organizer)
try: try:
self.request.event.settings.set('banktransfer_csvhint', hint) o.settings.set('banktransfer_csvhint', hint)
except Exception as e: # TODO: narrow down except Exception as e: # TODO: narrow down
logger.error('Import using stored hint failed: ' + str(e)) logger.error('Import using stored hint failed: ' + str(e))
pass pass
@@ -321,44 +356,117 @@ class ImportView(EventPermissionRequiredMixin, ListView):
return super().get(self.request) return super().get(self.request)
def assign_view(self, parsed): def assign_view(self, parsed):
return render(self.request, 'pretixplugins/banktransfer/import_assign.html', { ctx = {'rows': parsed}
'rows': parsed if 'event' in self.kwargs:
}) ctx['basetpl'] = 'pretixplugins/banktransfer/import_base.html'
else:
ctx['basetpl'] = 'pretixplugins/banktransfer/import_base_organizer.html'
ctx['organizer'] = self.request.organizer
return render(self.request, 'pretixplugins/banktransfer/import_assign.html', ctx)
@cached_property @cached_property
def job_running(self): def job_running(self):
return BankImportJob.objects.filter( if 'event' in self.kwargs:
event=self.request.event, state=BankImportJob.STATE_RUNNING, qs = BankImportJob.objects.filter(
Q(event=self.request.event) | Q(organizer=self.request.organizer)
)
else:
qs = BankImportJob.objects.filter(
Q(organizer=self.request.organizer)
)
return qs.filter(
state=BankImportJob.STATE_RUNNING,
created__lte=now() - timedelta(minutes=30) # safety timeout created__lte=now() - timedelta(minutes=30) # safety timeout
).first() ).first()
def redirect_back(self): def redirect_back(self):
return redirect(reverse('plugins:banktransfer:import', kwargs={ kwargs = {
'event': self.request.event.slug, 'organizer': self.request.organizer.slug
'organizer': self.request.event.organizer.slug, }
})) if 'event' in self.kwargs:
kwargs['event'] = self.kwargs['event']
return redirect(reverse('plugins:banktransfer:import', kwargs=kwargs))
def start_processing(self, parsed): def start_processing(self, parsed):
if self.job_running: if self.job_running:
messages.error(self.request, _('An import is currently being processed, please try again in a few minutes.')) messages.error(self.request,
_('An import is currently being processed, please try again in a few minutes.'))
return self.redirect_back() return self.redirect_back()
job = BankImportJob.objects.create(event=self.request.event) if 'event' in self.kwargs:
job = BankImportJob.objects.create(event=self.request.event, organizer=self.request.organizer)
else:
job = BankImportJob.objects.create(organizer=self.request.organizer)
process_banktransfers.apply_async(kwargs={ process_banktransfers.apply_async(kwargs={
'event': self.request.event.pk,
'job': job.pk, 'job': job.pk,
'data': parsed 'data': parsed
}) })
return redirect(reverse('plugins:banktransfer:import.job', kwargs={ kwargs = {
'event': self.request.event.slug, 'organizer': self.request.organizer.slug,
'organizer': self.request.event.organizer.slug,
'job': job.pk 'job': job.pk
})) }
if 'event' in self.kwargs:
kwargs['event'] = self.kwargs['event']
return redirect(reverse('plugins:banktransfer:import.job', kwargs=kwargs))
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data() ctx = super().get_context_data()
ctx['job_running'] = self.job_running ctx['job_running'] = self.job_running
ctx['no_more_payments'] = False ctx['no_more_payments'] = False
if self.request.event.settings.get('payment_term_last'):
if now() > self.request.event.payment_term_last: if 'event' in self.kwargs:
ctx['no_more_payments'] = True ctx['basetpl'] = 'pretixplugins/banktransfer/import_base.html'
if self.request.event.settings.get('payment_term_last'):
if now() > self.request.event.payment_term_last:
ctx['no_more_payments'] = True
else:
ctx['basetpl'] = 'pretixplugins/banktransfer/import_base_organizer.html'
ctx['organizer'] = self.request.organizer
return ctx return ctx
class OrganizerBanktransferView:
def dispatch(self, request, *args, **kwargs):
if len(request.organizer.events.order_by('currency').values_list('currency', flat=True).distinct()) > 1:
messages.error(request, _('Please perform per-event bank imports as this organizer has events with '
'multuple currencies.'))
return redirect('control:organizer', organizer=request.organizer.slug)
return super().dispatch(request, *args, **kwargs)
class EventImportView(EventPermissionRequiredMixin, ImportView):
permission = 'can_change_orders'
class OrganizerImportView(OrganizerBanktransferView, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin,
ImportView):
permission = 'can_change_orders'
class EventJobDetailView(EventPermissionRequiredMixin, JobDetailView):
permission = 'can_change_orders'
class OrganizerJobDetailView(OrganizerBanktransferView, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin,
JobDetailView):
permission = 'can_change_orders'
class EventActionView(EventPermissionRequiredMixin, ActionView):
permission = 'can_change_orders'
class OrganizerActionView(OrganizerBanktransferView, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin,
ActionView):
permission = 'can_change_orders'
def order_qs(self):
all = self.request.user.teams.filter(organizer=self.request.organizer, can_change_orders=True,
can_view_orders=True, all_events=True).exists()
if self.request.user.is_superuser or all:
return Order.objects.filter(event__organizer=self.request.organizer)
else:
return Order.objects.filter(
event_id__in=self.request.user.teams.filter(
organizer=self.request.organizer, can_change_orders=True, can_view_orders=True
).values_list('limit_events__id', flat=True)
)
@@ -208,3 +208,68 @@ def test_retry_paid(env, client):
assert trans.state == BankTransaction.STATE_ERROR assert trans.state == BankTransaction.STATE_ERROR
env[3].refresh_from_db() env[3].refresh_from_db()
assert env[3].status == Order.STATUS_PAID assert env[3].status == Order.STATUS_PAID
@pytest.mark.django_db
def test_assign_order_organizer(env, client):
job = BankImportJob.objects.create(organizer=env[0].organizer)
trans = BankTransaction.objects.create(organizer=env[0].organizer, import_job=job, payer='Foo',
state=BankTransaction.STATE_NOMATCH,
amount=23, date='unknown')
client.login(email='dummy@dummy.dummy', password='dummy')
r = json.loads(client.post('/control/organizer/{}/banktransfer/action/'.format(env[0].organizer.slug), {
'action_{}'.format(trans.pk): 'assign:{}'.format(env[2].code),
}).content.decode('utf-8'))
assert r['status'] == 'ok'
trans.refresh_from_db()
assert trans.state == BankTransaction.STATE_VALID
env[2].refresh_from_db()
assert env[2].status == Order.STATUS_PAID
@pytest.mark.django_db
def test_assign_order_organizer_full_code(env, client):
job = BankImportJob.objects.create(organizer=env[0].organizer)
trans = BankTransaction.objects.create(organizer=env[0].organizer, import_job=job, payer='Foo',
state=BankTransaction.STATE_NOMATCH,
amount=23, date='unknown')
client.login(email='dummy@dummy.dummy', password='dummy')
r = json.loads(client.post('/control/organizer/{}/banktransfer/action/'.format(env[0].organizer.slug), {
'action_{}'.format(trans.pk): 'assign:{}-{}'.format(env[0].slug.upper(), env[2].code),
}).content.decode('utf-8'))
assert r['status'] == 'ok'
trans.refresh_from_db()
assert trans.state == BankTransaction.STATE_VALID
env[2].refresh_from_db()
assert env[2].status == Order.STATUS_PAID
@pytest.mark.django_db
def test_assign_order_organizer_no_permission(env, client):
job = BankImportJob.objects.create(organizer=env[0].organizer)
trans = BankTransaction.objects.create(organizer=env[0].organizer, import_job=job, payer='Foo',
state=BankTransaction.STATE_NOMATCH,
amount=23, date='unknown')
team = env[1].teams.first()
team.can_change_orders = False
team.save()
client.login(email='dummy@dummy.dummy', password='dummy')
r = client.post('/control/organizer/{}/banktransfer/action/'.format(env[0].organizer.slug), {
'action_{}'.format(trans.pk): 'assign:{}-{}'.format(env[0].slug.upper(), env[2].code),
})
assert r.status_code == 403
@pytest.mark.django_db
def test_assign_order_organizer_no_permission_for_event(env, client):
job = BankImportJob.objects.create(organizer=env[0].organizer)
trans = BankTransaction.objects.create(organizer=env[0].organizer, import_job=job, payer='Foo',
state=BankTransaction.STATE_NOMATCH,
amount=23, date='unknown')
team = env[1].teams.first()
team.limit_events.clear()
client.login(email='dummy@dummy.dummy', password='dummy')
r = json.loads(client.post('/control/organizer/{}/banktransfer/action/'.format(env[0].organizer.slug), {
'action_{}'.format(trans.pk): 'assign:{}-{}'.format(env[0].slug.upper(), env[2].code),
}).content.decode('utf-8'))
assert r['status'] == 'error'
+52 -5
View File
@@ -90,9 +90,14 @@ def job(env):
return BankImportJob.objects.create(event=env[0]).pk return BankImportJob.objects.create(event=env[0]).pk
@pytest.fixture
def orga_job(env):
return BankImportJob.objects.create(organizer=env[0].organizer).pk
@pytest.mark.django_db @pytest.mark.django_db
def test_mark_paid(env, job): def test_mark_paid(env, job):
process_banktransfers(env[0].pk, job, [{ process_banktransfers(job, [{
'payer': 'Karla Kundin', 'payer': 'Karla Kundin',
'reference': 'Bestellung DUMMY1234S', 'reference': 'Bestellung DUMMY1234S',
'date': '2016-01-26', 'date': '2016-01-26',
@@ -104,7 +109,7 @@ def test_mark_paid(env, job):
@pytest.mark.django_db @pytest.mark.django_db
def test_check_amount(env, job): def test_check_amount(env, job):
process_banktransfers(env[0].pk, job, [{ process_banktransfers(job, [{
'payer': 'Karla Kundin', 'payer': 'Karla Kundin',
'reference': 'Bestellung DUMMY1Z3AS', 'reference': 'Bestellung DUMMY1Z3AS',
'date': '2016-01-26', 'date': '2016-01-26',
@@ -116,7 +121,7 @@ def test_check_amount(env, job):
@pytest.mark.django_db @pytest.mark.django_db
def test_ignore_canceled(env, job): def test_ignore_canceled(env, job):
process_banktransfers(env[0].pk, job, [{ process_banktransfers(job, [{
'payer': 'Karla Kundin', 'payer': 'Karla Kundin',
'reference': 'Bestellung DUMMY6789Z', 'reference': 'Bestellung DUMMY6789Z',
'date': '2016-01-26', 'date': '2016-01-26',
@@ -128,7 +133,7 @@ def test_ignore_canceled(env, job):
@pytest.mark.django_db @pytest.mark.django_db
def test_autocorrection(env, job): def test_autocorrection(env, job):
process_banktransfers(env[0].pk, job, [{ process_banktransfers(job, [{
'payer': 'Karla Kundin', 'payer': 'Karla Kundin',
'reference': 'Bestellung DUMMY12345', 'reference': 'Bestellung DUMMY12345',
'amount': '23.00', 'amount': '23.00',
@@ -142,7 +147,7 @@ def test_autocorrection(env, job):
def test_huge_amount(env, job): def test_huge_amount(env, job):
env[2].total = Decimal('23000.00') env[2].total = Decimal('23000.00')
env[2].save() env[2].save()
process_banktransfers(env[0].pk, job, [{ process_banktransfers(job, [{
'payer': 'Karla Kundin', 'payer': 'Karla Kundin',
'reference': 'Bestellung DUMMY12345', 'reference': 'Bestellung DUMMY12345',
'amount': '23.000,00', 'amount': '23.000,00',
@@ -150,3 +155,45 @@ def test_huge_amount(env, job):
}]) }])
env[2].refresh_from_db() env[2].refresh_from_db()
assert env[2].status == Order.STATUS_PAID assert env[2].status == Order.STATUS_PAID
@pytest.mark.django_db
def test_mark_paid_organizer(env, orga_job):
process_banktransfers(orga_job, [{
'payer': 'Karla Kundin',
'reference': 'Bestellung DUMMY-1234S',
'date': '2016-01-26',
'amount': '23.00'
}])
env[2].refresh_from_db()
assert env[2].status == Order.STATUS_PAID
@pytest.mark.django_db
def test_mark_paid_organizer_weird_slug(env, orga_job):
env[0].slug = 'du.m-y'
env[0].save()
process_banktransfers(orga_job, [{
'payer': 'Karla Kundin',
'reference': 'Bestellung DU.M-Y-1234S',
'date': '2016-01-26',
'amount': '23.00'
}])
env[2].refresh_from_db()
assert env[2].status == Order.STATUS_PAID
@pytest.mark.django_db
def test_wrong_event_organizer(env, orga_job):
Event.objects.create(
organizer=env[0].organizer, name='Wrong', slug='wrong',
date_from=now(), plugins='pretix.plugins.banktransfer'
)
process_banktransfers(orga_job, [{
'payer': 'Karla Kundin',
'reference': 'Bestellung WRONG-1234S',
'date': '2016-01-26',
'amount': '23.00'
}])
env[2].refresh_from_db()
assert env[2].status == Order.STATUS_PENDING