From d446191cf427c8460bf6ca5529fcfbbe9104fe75 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 19 Jun 2017 13:43:13 +0200 Subject: [PATCH] Organizer-level bank import --- .../migrations/0004_auto_20170619_1125.py | 41 ++++ src/pretix/plugins/banktransfer/models.py | 15 +- src/pretix/plugins/banktransfer/signals.py | 21 +- src/pretix/plugins/banktransfer/tasks.py | 73 ++++-- .../banktransfer/import_assign.html | 2 +- .../banktransfer/import_base.html | 1 - .../banktransfer/import_base_organizer.html | 4 + .../banktransfer/import_form.html | 14 +- .../banktransfer/job_detail.html | 6 +- .../banktransfer/transaction_list.html | 12 +- src/pretix/plugins/banktransfer/urls.py | 15 +- src/pretix/plugins/banktransfer/views.py | 212 +++++++++++++----- .../plugins/banktransfer/test_actions.py | 65 ++++++ src/tests/plugins/banktransfer/test_import.py | 57 ++++- 14 files changed, 444 insertions(+), 94 deletions(-) create mode 100644 src/pretix/plugins/banktransfer/migrations/0004_auto_20170619_1125.py create mode 100644 src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_base_organizer.html diff --git a/src/pretix/plugins/banktransfer/migrations/0004_auto_20170619_1125.py b/src/pretix/plugins/banktransfer/migrations/0004_auto_20170619_1125.py new file mode 100644 index 0000000000..c53d7917fd --- /dev/null +++ b/src/pretix/plugins/banktransfer/migrations/0004_auto_20170619_1125.py @@ -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')]), + ), + ] diff --git a/src/pretix/plugins/banktransfer/models.py b/src/pretix/plugins/banktransfer/models.py index 250d991972..ee12af0aa7 100644 --- a/src/pretix/plugins/banktransfer/models.py +++ b/src/pretix/plugins/banktransfer/models.py @@ -16,10 +16,18 @@ class BankImportJob(models.Model): (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) 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): STATE_UNCHECKED = 'imported' @@ -40,7 +48,8 @@ class BankTransaction(models.Model): (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') state = models.CharField(max_length=32, choices=STATES, default=STATE_UNCHECKED) message = models.TextField() @@ -66,4 +75,4 @@ class BankTransaction(models.Model): self.reference = "" class Meta: - unique_together = ('event', 'checksum') + unique_together = ('event', 'organizer', 'checksum') diff --git a/src/pretix/plugins/banktransfer/signals.py b/src/pretix/plugins/banktransfer/signals.py index f5c5c1e5a4..423caa24c5 100644 --- a/src/pretix/plugins/banktransfer/signals.py +++ b/src/pretix/plugins/banktransfer/signals.py @@ -4,7 +4,7 @@ from django.template.loader import get_template from django.utils.translation import ugettext_lazy as _ 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 @@ -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") def html_head_presale(sender, request=None, **kwargs): url = resolve(request.path_info) diff --git a/src/pretix/plugins/banktransfer/tasks.py b/src/pretix/plugins/banktransfer/tasks.py index 00739c7a40..0211271433 100644 --- a/src/pretix/plugins/banktransfer/tasks.py +++ b/src/pretix/plugins/banktransfer/tasks.py @@ -6,10 +6,11 @@ from decimal import Decimal from celery.exceptions import MaxRetriesExceededError from django.conf import settings from django.db import transaction +from django.db.models import Q from django.utils.translation import ugettext_noop 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.locking import LockTimeoutException from pretix.base.services.mail import SendMailException @@ -21,17 +22,33 @@ from .models import BankImportJob, BankTransaction logger = logging.getLogger(__name__) -def _handle_transaction(event: Event, trans: BankTransaction, code: str): - try: - trans.order = event.orders.get(code=code) - except Order.DoesNotExist: - normalized_code = Order.normalize_code(code) +def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, organizer: Organizer=None, + slug: str=None): + if event: try: - trans.order = event.orders.get(code=normalized_code) + trans.order = event.orders.get(code=code) except Order.DoesNotExist: - trans.state = BankTransaction.STATE_NOMATCH - trans.save() - return + normalized_code = Order.normalize_code(code) + try: + 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: trans.state = BankTransaction.STATE_DUPLICATE @@ -63,9 +80,11 @@ def _handle_transaction(event: Event, trans: BankTransaction, code: str): 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.-]") - 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 = [] 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)) amount = Decimal("0.00") - trans = BankTransaction(event=event, import_job=job, + trans = BankTransaction(event=event, organizer=organizer, import_job=job, payer=row.get('payer', ''), reference=row['reference'], 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) -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 - event = Event.objects.get(pk=event) job = BankImportJob.objects.get(pk=job) job.state = BankImportJob.STATE_RUNNING job.save() + prefixes = [] try: # 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'] - 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: match = pattern.search(trans.reference.upper()) if match: - code = match.group(1) - with transaction.atomic(): - _handle_transaction(event, trans, code) + if job.event: + code = match.group(1) + 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: trans.state = BankTransaction.STATE_NOMATCH trans.save() diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_assign.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_assign.html index 260dfa2e63..c35908324e 100644 --- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_assign.html +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_assign.html @@ -1,4 +1,4 @@ -{% extends "pretixplugins/banktransfer/import_base.html" %} +{% extends basetpl %} {% load i18n %} {% block inner %}

{% blocktrans trimmed %} diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_base.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_base.html index b391089e7d..91c0e35b0a 100644 --- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_base.html +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_base.html @@ -6,5 +6,4 @@

{% trans "Import bank data" %}

{% block inner %} {% endblock %} - {% endblock %} diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_base_organizer.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_base_organizer.html new file mode 100644 index 0000000000..e9dd37e7f5 --- /dev/null +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_base_organizer.html @@ -0,0 +1,4 @@ +{% extends "pretixcontrol/organizers/base.html" %} +{% load i18n %} +{% load static %} +{% block title %}{% trans "Import bank data" %}{% endblock %} diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html index 4bd4ec9c5b..40f680ef80 100644 --- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html @@ -1,6 +1,7 @@ -{% extends "pretixplugins/banktransfer/import_base.html" %} +{% extends basetpl %} {% load i18n %} {% load bootstrap3 %} +{% load static %} {% block inner %} {% if no_more_payments %}
@@ -47,6 +48,16 @@

{% trans "Unresolved transactions" %}

+ {% if request.event %} +

+ {% 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 %} + {% trans "Go to organizer-level import" %} +

+ {% endif %}

{% endif %} + {% endblock %} diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/job_detail.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/job_detail.html index c7422b5244..b6967f559b 100644 --- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/job_detail.html +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/job_detail.html @@ -1,5 +1,6 @@ -{% extends "pretixplugins/banktransfer/import_base.html" %} +{% extends basetpl %} {% load i18n %} +{% load staticfiles %} {% block inner %}

{% trans "Import result" %}

{% if job.state == "running" or job.state == "pending" %} @@ -37,11 +38,12 @@ {% if transactions_ignored or transactions_invalid %} - {% trans "Review invalid and ignored payments" %} » {% endif %} {% endif %} + {% endblock %} diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/transaction_list.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/transaction_list.html index c873e7f475..ccd2f1807e 100644 --- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/transaction_list.html +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/transaction_list.html @@ -2,7 +2,7 @@ {% load staticfiles %}
{% csrf_token %} - +
@@ -83,9 +83,13 @@ diff --git a/src/pretix/plugins/banktransfer/urls.py b/src/pretix/plugins/banktransfer/urls.py index 3bc5b580c7..55c15b8119 100644 --- a/src/pretix/plugins/banktransfer/urls.py +++ b/src/pretix/plugins/banktransfer/urls.py @@ -3,10 +3,19 @@ from django.conf.urls import url from . import views urlpatterns = [ - url(r'^control/event/(?P[^/]+)/(?P[^/]+)/banktransfer/import/', views.ImportView.as_view(), + url(r'^control/organizer/(?P[^/]+)/banktransfer/import/', + views.OrganizerImportView.as_view(), + name='import'), + url(r'^control/organizer/(?P[^/]+)/banktransfer/job/(?P\d+)/', + views.OrganizerJobDetailView.as_view(), name='import.job'), + url(r'^control/organizer/(?P[^/]+)/banktransfer/action/', + views.OrganizerActionView.as_view(), name='import.action'), + + url(r'^control/event/(?P[^/]+)/(?P[^/]+)/banktransfer/import/', + views.EventImportView.as_view(), name='import'), url(r'^control/event/(?P[^/]+)/(?P[^/]+)/banktransfer/job/(?P\d+)/', - views.JobDetailView.as_view(), name='import.job'), + views.EventJobDetailView.as_view(), name='import.job'), url(r'^control/event/(?P[^/]+)/(?P[^/]+)/banktransfer/action/', - views.ActionView.as_view(), name='import.action'), + views.EventActionView.as_view(), name='import.action'), ] diff --git a/src/pretix/plugins/banktransfer/views.py b/src/pretix/plugins/banktransfer/views.py index 262c0a92ae..80007898b4 100644 --- a/src/pretix/plugins/banktransfer/views.py +++ b/src/pretix/plugins/banktransfer/views.py @@ -17,7 +17,10 @@ from pretix.base.models import Order, Quota from pretix.base.services.mail import SendMailException 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, OrganizerPermissionRequiredMixin, +) +from pretix.control.views.organizer import OrganizerDetailViewMixin from pretix.plugins.banktransfer import csvimport, mt940import from pretix.plugins.banktransfer.models import BankImportJob, BankTransaction 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') -class ActionView(EventPermissionRequiredMixin, View): +class ActionView(View): permission = 'can_change_orders' def _discard(self, trans): @@ -89,7 +92,10 @@ class ActionView(EventPermissionRequiredMixin, View): def _assign(self, trans, code): 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: return JsonResponse({ 'status': 'error', @@ -109,7 +115,10 @@ class ActionView(EventPermissionRequiredMixin, View): for k, v in request.POST.items(): if not k.startswith('action_'): 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, BankTransaction.STATE_NOMATCH, BankTransaction.STATE_DUPLICATE): @@ -140,52 +149,64 @@ class ActionView(EventPermissionRequiredMixin, View): if len(query) < 2: 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({ 'results': [ { - 'code': o.code, + 'code': o.event.slug.upper() + '-' + o.code, 'status': o.get_status_display(), - 'total': localize(o.total) + ' ' + self.request.event.currency + 'total': localize(o.total) + ' ' + o.event.currency } 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' permission = 'can_change_orders' context_objectname = 'job' def redirect_form(self): - return redirect(reverse('plugins:banktransfer:import', kwargs={ - 'event': self.request.event.slug, - 'organizer': self.request.event.organizer.slug, - })) + kwargs = { + 'organizer': self.request.organizer.slug, + } + if 'event' in self.kwargs: + kwargs['event'] = self.kwargs['event'] + return redirect(reverse('plugins:banktransfer:import', kwargs=kwargs)) def redirect_back(self): - return redirect(reverse('plugins:banktransfer:import.job', kwargs={ - 'event': self.request.event.slug, - 'organizer': self.request.event.organizer.slug, + kwargs = { + 'organizer': self.request.organizer.slug, '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): - return get_object_or_404(BankImportJob, id=self.kwargs['job'], event=self.request.event) + @cached_property + 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): if 'ajax' in request.GET: - self.object = self.get_object() 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): - 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_invalid'] = qs.filter(state__in=[ @@ -194,21 +215,33 @@ class JobDetailView(EventPermissionRequiredMixin, DetailView): ctx['transactions_ignored'] = qs.filter(state__in=[ BankTransaction.STATE_DUPLICATE, BankTransaction.STATE_NOMATCH ]).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 -class ImportView(EventPermissionRequiredMixin, ListView): +class ImportView(ListView): template_name = 'pretixplugins/banktransfer/import_form.html' permission = 'can_change_orders' context_object_name = 'transactions_unhandled' paginate_by = 30 def get_queryset(self): - qs = BankTransaction.objects.filter( - event=self.request.event - ).select_related('order').filter(state__in=[ + if 'event' in self.kwargs: + qs = BankTransaction.objects.filter( + 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_DUPLICATE, BankTransaction.STATE_NOMATCH ]) @@ -245,12 +278,12 @@ class ImportView(EventPermissionRequiredMixin, ListView): else: 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() @cached_property 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): try: @@ -261,6 +294,7 @@ class ImportView(EventPermissionRequiredMixin, ListView): return self.redirect_back() def process_csv_file(self): + o = getattr(self.request, 'event', self.request.organizer) try: data = csvimport.get_rows_from_file(self.request.FILES['file']) 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 ' 'contact support for help.')) - if self.request.event.settings.get('banktransfer_csvhint') is not None: - hint = self.request.event.settings.get('banktransfer_csvhint', as_type=dict) + if o.settings.get('banktransfer_csvhint') is not None: + hint = o.settings.get('banktransfer_csvhint', as_type=dict) try: parsed, good = csvimport.parse(data, hint) @@ -304,8 +338,9 @@ class ImportView(EventPermissionRequiredMixin, ListView): logger.error('Parsing hint failed: ' + str(e)) messages.error(self.request, _('We were unable to process your input.')) return self.assign_view(data) + o = getattr(self.request, 'event', self.request.organizer) try: - self.request.event.settings.set('banktransfer_csvhint', hint) + o.settings.set('banktransfer_csvhint', hint) except Exception as e: # TODO: narrow down logger.error('Import using stored hint failed: ' + str(e)) pass @@ -321,44 +356,117 @@ class ImportView(EventPermissionRequiredMixin, ListView): return super().get(self.request) def assign_view(self, parsed): - return render(self.request, 'pretixplugins/banktransfer/import_assign.html', { - 'rows': parsed - }) + ctx = {'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 def job_running(self): - return BankImportJob.objects.filter( - event=self.request.event, state=BankImportJob.STATE_RUNNING, + if 'event' in self.kwargs: + 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 ).first() def redirect_back(self): - return redirect(reverse('plugins:banktransfer:import', kwargs={ - 'event': self.request.event.slug, - 'organizer': self.request.event.organizer.slug, - })) + kwargs = { + 'organizer': self.request.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): 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() - 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={ - 'event': self.request.event.pk, 'job': job.pk, 'data': parsed }) - return redirect(reverse('plugins:banktransfer:import.job', kwargs={ - 'event': self.request.event.slug, - 'organizer': self.request.event.organizer.slug, + kwargs = { + 'organizer': self.request.organizer.slug, '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): ctx = super().get_context_data() ctx['job_running'] = self.job_running ctx['no_more_payments'] = False - if self.request.event.settings.get('payment_term_last'): - if now() > self.request.event.payment_term_last: - ctx['no_more_payments'] = True + + if 'event' in self.kwargs: + 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 + + +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) + ) diff --git a/src/tests/plugins/banktransfer/test_actions.py b/src/tests/plugins/banktransfer/test_actions.py index 69ca64198b..773f519cde 100644 --- a/src/tests/plugins/banktransfer/test_actions.py +++ b/src/tests/plugins/banktransfer/test_actions.py @@ -208,3 +208,68 @@ def test_retry_paid(env, client): assert trans.state == BankTransaction.STATE_ERROR env[3].refresh_from_db() 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' diff --git a/src/tests/plugins/banktransfer/test_import.py b/src/tests/plugins/banktransfer/test_import.py index 17963f6e00..7f21d472ea 100644 --- a/src/tests/plugins/banktransfer/test_import.py +++ b/src/tests/plugins/banktransfer/test_import.py @@ -90,9 +90,14 @@ def job(env): 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 def test_mark_paid(env, job): - process_banktransfers(env[0].pk, job, [{ + process_banktransfers(job, [{ 'payer': 'Karla Kundin', 'reference': 'Bestellung DUMMY1234S', 'date': '2016-01-26', @@ -104,7 +109,7 @@ def test_mark_paid(env, job): @pytest.mark.django_db def test_check_amount(env, job): - process_banktransfers(env[0].pk, job, [{ + process_banktransfers(job, [{ 'payer': 'Karla Kundin', 'reference': 'Bestellung DUMMY1Z3AS', 'date': '2016-01-26', @@ -116,7 +121,7 @@ def test_check_amount(env, job): @pytest.mark.django_db def test_ignore_canceled(env, job): - process_banktransfers(env[0].pk, job, [{ + process_banktransfers(job, [{ 'payer': 'Karla Kundin', 'reference': 'Bestellung DUMMY6789Z', 'date': '2016-01-26', @@ -128,7 +133,7 @@ def test_ignore_canceled(env, job): @pytest.mark.django_db def test_autocorrection(env, job): - process_banktransfers(env[0].pk, job, [{ + process_banktransfers(job, [{ 'payer': 'Karla Kundin', 'reference': 'Bestellung DUMMY12345', 'amount': '23.00', @@ -142,7 +147,7 @@ def test_autocorrection(env, job): def test_huge_amount(env, job): env[2].total = Decimal('23000.00') env[2].save() - process_banktransfers(env[0].pk, job, [{ + process_banktransfers(job, [{ 'payer': 'Karla Kundin', 'reference': 'Bestellung DUMMY12345', 'amount': '23.000,00', @@ -150,3 +155,45 @@ def test_huge_amount(env, job): }]) env[2].refresh_from_db() 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
{% if trans.order %} - - {{ trans.order.code }} + + {% if not request.event %} + {{ trans.order.event.slug|upper }}-{{ trans.order.code }} + {% else %} + {{ trans.order.code }} + {% endif %} {% endif %}