Coverage for jbank/admin.py : 51%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# pylint: disable=too-many-arguments
2import base64
3import logging
4import os
5import traceback
6from datetime import datetime
7from os.path import basename
8from typing import Optional
10import pytz
11from django import forms
12from django.conf import settings
13from django.conf.urls import url
14from django.contrib import admin
15from django.contrib import messages
16from django.contrib.admin import SimpleListFilter
17from django.contrib.auth.models import User
18from django.contrib.messages import add_message, ERROR
19from django.core.exceptions import ValidationError
20from django.core.files.uploadedfile import InMemoryUploadedFile
21from django.db import transaction
22from django.db.models import F, Q
23from django.db.models.aggregates import Sum
24from django.http import HttpRequest, Http404
25from django.shortcuts import render, get_object_or_404
26from django.urls import ResolverMatch, reverse
27from django.utils.formats import date_format
28from django.utils.html import format_html
29from django.utils.safestring import mark_safe
30from django.utils.text import capfirst
31from django.utils.translation import gettext_lazy as _
32from jacc.models import Account, EntryType
33from jbank.x509_helpers import get_x509_cert_from_file
34from jutil.responses import FormattedXmlResponse, FormattedXmlFileResponse
35from jutil.xml import xml_to_dict
36from jbank.helpers import create_statement, create_reference_payment_batch
37from jbank.models import Statement, StatementRecord, StatementRecordSepaInfo, ReferencePaymentRecord, \
38 ReferencePaymentBatch, StatementFile, ReferencePaymentBatchFile, Payout, Refund, PayoutStatus, PayoutParty, \
39 StatementRecordDetail, StatementRecordRemittanceInfo, CurrencyExchange, CurrencyExchangeSource, WsEdiConnection, \
40 WsEdiSoapCall
41from jbank.parsers import parse_tiliote_statements, parse_tiliote_statements_from_file, parse_svm_batches_from_file, \
42 parse_svm_batches
43from jutil.admin import ModelAdminBase, admin_log
45logger = logging.getLogger(__name__)
48class SettlementEntryTypesFilter(SimpleListFilter):
49 """
50 Filters incoming settlement type entries.
51 """
52 title = _('account entry types')
53 parameter_name = 'type'
55 def lookups(self, request, model_admin):
56 choices = []
57 for e in EntryType.objects.all().filter(is_settlement=True).order_by('name'):
58 assert isinstance(e, EntryType)
59 choices.append((e.id, capfirst(e.name)))
60 return choices
62 def queryset(self, request, queryset):
63 val = self.value()
64 if val:
65 return queryset.filter(type__id=val)
66 return queryset
69class AccountEntryMatchedFilter(SimpleListFilter):
70 """
71 Filters incoming payments which do not have any child/derived account entries.
72 """
73 title = _('account.entry.matched.filter')
74 parameter_name = 'matched'
76 def lookups(self, request, model_admin):
77 return [
78 ('1', capfirst(_('account.entry.not.matched'))),
79 ('2', capfirst(_('account.entry.is.matched'))),
80 ('3', capfirst(_('marked as settled'))),
81 ]
83 def queryset(self, request, queryset):
84 val = self.value()
85 if val:
86 # return original settlements only
87 queryset = queryset.filter(type__is_settlement=True, parent=None)
88 if val == '1':
89 # return those which are not manually settled and
90 # have either a) no children b) sum of children less than amount
91 queryset = queryset.exclude(manually_settled=True)
92 queryset = queryset.annotate(child_set_amount=Sum('child_set__amount'))
93 return queryset.filter(Q(child_set=None) | Q(child_set_amount__lt=F('amount')))
94 if val == '2':
95 # return any entries with derived account entries or marked as manually settled
96 return queryset.exclude(Q(child_set=None) & Q(manually_settled=False))
97 if val == '3':
98 # return only manually marked as settled
99 return queryset.filter(manually_settled=True)
100 return queryset
103class AccountNameFilter(SimpleListFilter):
104 """
105 Filters account entries based on account name.
106 """
107 title = _('account.name.filter')
108 parameter_name = 'account-name'
110 def lookups(self, request, model_admin):
111 ops = []
112 qs = model_admin.get_queryset(request)
113 for e in qs.distinct('account__name'):
114 ops.append((e.account.name, e.account.name))
115 return sorted(ops, key=lambda x: x[0])
117 def queryset(self, request, queryset):
118 val = self.value()
119 if val:
120 return queryset.filter(account__name=val)
121 return queryset
124class StatementAdmin(ModelAdminBase):
125 exclude = ()
126 list_per_page = 20
127 save_on_top = False
128 ordering = ('-record_date', 'account_number')
129 date_hierarchy = 'record_date'
130 list_filter = (
131 'account_number',
132 )
133 readonly_fields = (
134 'file_link',
135 'account_number',
136 'statement_number',
137 'begin_date',
138 'end_date',
139 'record_date',
140 'customer_identifier',
141 'begin_balance_date',
142 'begin_balance',
143 'record_count',
144 'currency_code',
145 'account_name',
146 'account_limit',
147 'owner_name',
148 'contact_info_1',
149 'contact_info_2',
150 'bank_specific_info_1',
151 'iban',
152 'bic',
153 )
154 fields = readonly_fields
155 search_fields = (
156 'name',
157 'statement_number',
158 )
159 list_display = (
160 'id',
161 'record_date',
162 'account_number',
163 'statement_number',
164 'begin_balance',
165 'currency_code',
166 'file_link',
167 'account_entry_list'
168 )
170 def account_entry_list(self, obj):
171 assert isinstance(obj, Statement)
172 admin_url = reverse('admin:jbank_statementrecord_statement_changelist', args=(obj.id, ))
173 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), StatementRecord.objects.filter(statement=obj).count())
174 account_entry_list.short_description = _('account entries') # type: ignore
176 def file_link(self, obj):
177 assert isinstance(obj, Statement)
178 if not obj.file:
179 return ''
180 admin_url = reverse('admin:jbank_statementfile_change', args=(obj.file.id, ))
181 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.name)
182 file_link.admin_order_field = 'file' # type: ignore
183 file_link.short_description = _('file') # type: ignore
185 def get_urls(self):
186 return [
187 url(r'^by-file/(?P<file_id>\d+)/$', self.admin_site.admin_view(self.kw_changelist_view), name='jbank_statement_file_changelist'),
188 ] + super().get_urls()
190 def get_queryset(self, request: HttpRequest):
191 rm = request.resolver_match
192 assert isinstance(rm, ResolverMatch)
193 qs = super().get_queryset(request)
194 file_id = rm.kwargs.get('file_id', None)
195 if file_id:
196 qs = qs.filter(file=file_id)
197 return qs
200class StatementRecordDetailInlineAdmin(admin.StackedInline):
201 exclude = ()
202 model = StatementRecordDetail
203 can_delete = False
204 extra = 0
206 fields = (
207 'batch_identifier',
208 'amount',
209 'currency_code',
210 'instructed_amount',
211 'exchange',
212 'archive_identifier',
213 'end_to_end_identifier',
214 'creditor_name',
215 'debtor_name',
216 'ultimate_debtor_name',
217 'unstructured_remittance_info',
218 'paid_date',
219 'structured_remittance_info',
220 )
221 readonly_fields = fields
222 raw_id_fields = (
223 )
225 def structured_remittance_info(self, obj):
226 assert isinstance(obj, StatementRecordDetail)
227 lines = []
228 for rinfo in obj.remittanceinfo_set.all().order_by('id'):
229 assert isinstance(rinfo, StatementRecordRemittanceInfo)
230 lines.append(str(rinfo))
231 return mark_safe('<br>'.join(lines))
232 structured_remittance_info.short_description = _('structured remittance info') # type: ignore
234 def has_add_permission(self, request, obj=None):
235 return False
238class StatementRecordSepaInfoInlineAdmin(admin.StackedInline):
239 exclude = ()
240 model = StatementRecordSepaInfo
241 can_delete = False
242 extra = 0
243 max_num = 1
245 readonly_fields = (
246 'record',
247 'reference',
248 'iban_account_number',
249 'bic_code',
250 'recipient_name_detail',
251 'payer_name_detail',
252 'identifier',
253 'archive_identifier',
254 )
255 raw_id_fields = (
256 'record',
257 )
259 def has_add_permission(self, request, obj=None): # pylint: disable=unused-argument
260 return False
263def mark_as_manually_settled(modeladmin, request, qs): # pylint: disable=unused-argument
264 try:
265 data = request.POST.dict()
267 if 'description' in data:
268 description = data['description']
269 user = request.user
270 for e in list(qs.filter(manually_settled=False)):
271 e.manually_settled = True
272 e.save(update_fields=['manually_settled'])
273 msg = '{}: {}'.format(capfirst(_('marked as manually settled')), description)
274 admin_log([e], msg, who=user)
275 messages.info(request, msg)
276 else:
277 cx = {
278 'qs': qs,
279 }
280 return render(request, 'admin/jbank/mark_as_manually_settled.html', context=cx)
281 except ValidationError as e:
282 messages.error(request, ' '.join(e.messages))
283 except Exception as e:
284 logger.error('mark_as_manually_settled: %s', traceback.format_exc())
285 messages.error(request, '{}'.format(e))
286 return None
289def unmark_manually_settled_flag(modeladmin, request, qs): # pylint: disable=unused-argument
290 user = request.user
291 for e in list(qs.filter(manually_settled=True)):
292 e.manually_settled = False
293 e.save(update_fields=['manually_settled'])
294 msg = capfirst(_('manually settled flag cleared'))
295 admin_log([e], msg, who=user)
296 messages.info(request, msg)
299class StatementRecordAdmin(ModelAdminBase):
300 exclude = ()
301 list_per_page = 25
302 save_on_top = False
303 date_hierarchy = 'record_date'
304 readonly_fields = (
305 'id',
306 'entry_type',
307 'statement',
308 'line_number',
309 'file_link',
310 'record_number',
311 'archive_identifier',
312 'record_date',
313 'value_date',
314 'paid_date',
315 'type',
316 'record_code',
317 'record_domain',
318 'family_code',
319 'sub_family_code',
320 'record_description',
321 'amount',
322 'receipt_code',
323 'delivery_method',
324 'name',
325 'name_source',
326 'recipient_account_number',
327 'recipient_account_number_changed',
328 'remittance_info',
329 'messages',
330 'client_messages',
331 'bank_messages',
332 'archived',
333 'manually_settled',
334 # from AccountEntry
335 'account',
336 'timestamp',
337 'created',
338 'last_modified',
339 'type',
340 'description',
341 'amount',
342 'source_file',
343 'source_invoice',
344 'settled_invoice',
345 'settled_item',
346 'parent',
347 )
348 raw_id_fields = (
349 'statement',
350 # from AccountEntry
351 'account',
352 'source_file',
353 'parent',
354 'source_invoice',
355 'settled_invoice',
356 'settled_item',
357 )
358 list_filter = (
359 'statement__file__tag',
360 AccountNameFilter,
361 AccountEntryMatchedFilter,
362 SettlementEntryTypesFilter,
363 'record_code',
364 )
365 search_fields = (
366 '=archive_identifier',
367 '=amount',
368 '=recipient_account_number',
369 'record_description',
370 'name',
371 'remittance_info',
372 'messages',
373 )
374 list_display = (
375 'id',
376 'record_date',
377 'type',
378 'record_code',
379 'amount',
380 'name',
381 'recipient_account_number',
382 'remittance_info',
383 'source_file_link'
384 )
385 inlines = (
386 StatementRecordSepaInfoInlineAdmin,
387 StatementRecordDetailInlineAdmin,
388 )
389 actions = (
390 mark_as_manually_settled,
391 unmark_manually_settled_flag,
392 )
394 def get_urls(self):
395 return [
396 url(r'^by-statement/(?P<statement_id>\d+)/$', self.admin_site.admin_view(self.kw_changelist_view),
397 name='jbank_statementrecord_statement_changelist'),
398 ] + super().get_urls()
400 def get_queryset(self, request: HttpRequest):
401 rm = request.resolver_match
402 assert isinstance(rm, ResolverMatch)
403 qs = super().get_queryset(request)
404 statement_id = rm.kwargs.get('statement_id', None)
405 if statement_id:
406 qs = qs.filter(statement__id=statement_id)
407 return qs
409 def source_file_link(self, obj):
410 assert isinstance(obj, StatementRecord)
411 if not obj.statement:
412 return ''
413 admin_url = reverse('admin:jbank_statementfile_change', args=(obj.statement.file.id, ))
414 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.statement.name)
415 source_file_link.admin_order_field = 'statement' # type: ignore
416 source_file_link.short_description = _('source file') # type: ignore
418 def file_link(self, obj):
419 assert isinstance(obj, StatementRecord)
420 if not obj.statement or not obj.statement.file:
421 return ''
422 name = basename(obj.statement.file.file.name)
423 admin_url = reverse('admin:jbank_statementfile_change', args=(obj.statement.file.id, ))
424 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), name)
425 file_link.admin_order_field = 'file' # type: ignore
426 file_link.short_description = _("account statement file") # type: ignore
429class ReferencePaymentRecordAdmin(ModelAdminBase):
430 exclude = ()
431 list_per_page = 25
432 save_on_top = False
433 date_hierarchy = 'record_date'
434 raw_id_fields = (
435 'batch',
436 # from AccountEntry
437 'account',
438 'source_file',
439 'parent',
440 'source_invoice',
441 'settled_invoice',
442 'settled_item',
443 )
444 readonly_fields = (
445 'id',
446 'batch',
447 'line_number',
448 'file_link',
449 'record_type',
450 'account_number',
451 'record_date',
452 'paid_date',
453 'archive_identifier',
454 'remittance_info',
455 'payer_name',
456 'currency_identifier',
457 'name_source',
458 'amount',
459 'correction_identifier',
460 'delivery_method',
461 'receipt_code',
462 'archived',
463 'manually_settled',
464 # from AccountEntry
465 'account',
466 'timestamp',
467 'created',
468 'last_modified',
469 'type',
470 'description',
471 'amount',
472 'source_file',
473 'source_invoice',
474 'settled_invoice',
475 'settled_item',
476 'parent',
477 )
478 list_filter = (
479 'batch__file__tag',
480 AccountNameFilter,
481 AccountEntryMatchedFilter,
482 'correction_identifier',
483 )
484 search_fields = (
485 '=archive_identifier',
486 '=amount',
487 'remittance_info',
488 'payer_name',
489 'batch__name',
490 )
491 list_display = (
492 'id',
493 'record_date',
494 'type',
495 'amount',
496 'payer_name',
497 'remittance_info',
498 'source_file_link',
499 )
500 actions = (
501 mark_as_manually_settled,
502 unmark_manually_settled_flag,
503 )
505 def file_link(self, obj):
506 assert isinstance(obj, ReferencePaymentRecord)
507 if not obj.batch or not obj.batch.file:
508 return ''
509 admin_url = reverse('admin:jbank_referencepaymentbatchfile_change', args=(obj.batch.file.id, ))
510 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.batch.file)
511 file_link.admin_order_field = 'file' # type: ignore
512 file_link.short_description = _('file') # type: ignore
514 def get_urls(self):
515 return [
516 url(r'^by-batch/(?P<batch_id>\d+)/$', self.admin_site.admin_view(self.kw_changelist_view),
517 name='jbank_referencepaymentrecord_batch_changelist'),
518 ] + super().get_urls()
520 def get_queryset(self, request: HttpRequest):
521 rm = request.resolver_match
522 assert isinstance(rm, ResolverMatch)
523 qs = super().get_queryset(request)
524 batch_id = rm.kwargs.get('batch_id', None)
525 if batch_id:
526 qs = qs.filter(batch_id=batch_id)
527 return qs
529 def source_file_link(self, obj):
530 assert isinstance(obj, ReferencePaymentRecord)
531 if not obj.batch:
532 return ''
533 admin_url = reverse('admin:jbank_referencepaymentbatchfile_change', args=(obj.batch.file.id, ))
534 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.batch.name)
535 source_file_link.admin_order_field = 'batch' # type: ignore
536 source_file_link.short_description = _('account entry source file') # type: ignore
539class ReferencePaymentBatchAdmin(ModelAdminBase):
540 exclude = ()
541 list_per_page = 20
542 save_on_top = False
543 ordering = ('-record_date',)
544 date_hierarchy = 'record_date'
545 list_filter = (
546 'record_set__account_number',
547 )
548 fields = (
549 'file_link',
550 'record_date',
551 'institution_identifier',
552 'service_identifier',
553 'currency_identifier',
554 )
555 readonly_fields = (
556 'name',
557 'file',
558 'file_link',
559 'record_date',
560 'institution_identifier',
561 'service_identifier',
562 'currency_identifier',
563 )
564 search_fields = (
565 'name',
566 '=record_set__archive_identifier',
567 '=record_set__amount',
568 'record_set__remittance_info',
569 'record_set__payer_name',
570 )
571 list_display = (
572 'id',
573 'name',
574 'record_date',
575 'service_identifier',
576 'currency_identifier',
577 'account_entry_list',
578 )
580 def account_entry_list(self, obj):
581 assert isinstance(obj, ReferencePaymentBatch)
582 admin_url = reverse('admin:jbank_referencepaymentrecord_batch_changelist', args=(obj.id, ))
583 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), ReferencePaymentRecord.objects.filter(batch=obj).count())
584 account_entry_list.short_description = _('account entries') # type: ignore
586 def file_link(self, obj):
587 assert isinstance(obj, ReferencePaymentBatch)
588 if not obj.file:
589 return ''
590 admin_url = reverse('admin:jbank_referencepaymentbatchfile_change', args=(obj.file.id, ))
591 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.file)
592 file_link.admin_order_field = 'file' # type: ignore
593 file_link.short_description = _('file') # type: ignore
595 def get_urls(self):
596 return [
597 url(r'^by-file/(?P<file_id>\d+)/$', self.admin_site.admin_view(self.kw_changelist_view), name='jbank_referencepaymentbatch_file_changelist'),
598 ] + super().get_urls()
600 def get_queryset(self, request: HttpRequest):
601 rm = request.resolver_match
602 assert isinstance(rm, ResolverMatch)
603 qs = super().get_queryset(request)
604 file_id = rm.kwargs.get('file_id', None)
605 if file_id:
606 qs = qs.filter(file=file_id)
607 return qs
610class StatementFileForm(forms.ModelForm):
611 class Meta:
612 fields = [
613 'file',
614 ]
616 def clean_file(self):
617 file = self.cleaned_data['file']
618 assert isinstance(file, InMemoryUploadedFile)
619 name = file.name
620 file.seek(0)
621 content = file.read()
622 assert isinstance(content, bytes)
623 try:
624 statements = parse_tiliote_statements(content.decode('ISO-8859-1'), filename=basename(name))
625 for stm in statements:
626 account_number = stm['header']['account_number']
627 if Account.objects.filter(name=account_number).count() == 0:
628 raise ValidationError(_('account.not.found').format(account_number=account_number))
629 except ValidationError:
630 raise
631 except Exception as e:
632 raise ValidationError(_('Unhandled error') + ': {}'.format(e))
633 return file
636class StatementFileAdmin(ModelAdminBase):
637 save_on_top = False
638 exclude = ()
639 form = StatementFileForm
641 date_hierarchy = 'created'
643 search_fields = (
644 'original_filename__contains',
645 )
647 list_filter = (
648 'tag',
649 )
651 list_display = (
652 'id',
653 'created',
654 'file',
655 )
657 readonly_fields = (
658 'created',
659 'errors',
660 'file',
661 'original_filename',
662 )
664 def has_add_permission(self, request: HttpRequest) -> bool:
665 return False
667 def construct_change_message(self, request, form, formsets, add=False):
668 if add:
669 instance = form.instance
670 assert isinstance(instance, StatementFile)
671 if instance.file:
672 full_path = instance.full_path
673 plain_filename = basename(full_path)
674 try:
675 statements = parse_tiliote_statements_from_file(full_path)
676 with transaction.atomic():
677 for data in statements:
678 create_statement(data, name=plain_filename, file=instance)
679 except Exception as e:
680 instance.errors = traceback.format_exc()
681 instance.save()
682 add_message(request, ERROR, str(e))
683 instance.delete()
685 return super().construct_change_message(request, form, formsets, add)
688class ReferencePaymentBatchFileForm(forms.ModelForm):
689 class Meta:
690 fields = [
691 'file',
692 ]
694 def clean_file(self):
695 file = self.cleaned_data['file']
696 assert isinstance(file, InMemoryUploadedFile)
697 name = file.name
698 file.seek(0)
699 content = file.read()
700 assert isinstance(content, bytes)
701 try:
702 batches = parse_svm_batches(content.decode('ISO-8859-1'), filename=basename(name))
703 for b in batches:
704 for rec in b['records']:
705 account_number = rec['account_number']
706 if Account.objects.filter(name=account_number).count() == 0:
707 raise ValidationError(_('account.not.found').format(account_number=account_number))
708 except ValidationError:
709 raise
710 except Exception as e:
711 raise ValidationError(_('Unhandled error') + ': {}'.format(e))
712 return file
715class ReferencePaymentBatchFileAdmin(ModelAdminBase):
716 save_on_top = False
717 exclude = ()
718 form = ReferencePaymentBatchFileForm
719 date_hierarchy = 'created'
721 list_display = (
722 'id',
723 'created',
724 'file',
725 )
727 list_filter = (
728 'tag',
729 )
731 search_fields = (
732 'file__contains',
733 )
735 readonly_fields = (
736 'created',
737 'errors',
738 'file',
739 'original_filename',
740 )
742 def has_add_permission(self, request: HttpRequest) -> bool:
743 return False
745 def construct_change_message(self, request, form, formsets, add=False):
746 if add:
747 instance = form.instance
748 assert isinstance(instance, ReferencePaymentBatchFile)
749 if instance.file:
750 full_path = instance.full_path
751 plain_filename = basename(full_path)
752 try:
753 batches = parse_svm_batches_from_file(full_path)
754 with transaction.atomic():
755 for data in batches:
756 create_reference_payment_batch(data, name=plain_filename, file=instance)
757 except Exception as e:
758 user = request.user
759 assert isinstance(user, User)
760 instance.errors = traceback.format_exc()
761 instance.save()
762 msg = str(e)
763 if user.is_superuser:
764 msg = instance.errors
765 logger.error('%s: %s', plain_filename, msg)
766 add_message(request, ERROR, msg)
767 instance.delete()
769 return super().construct_change_message(request, form, formsets, add)
772class PayoutStatusAdmin(ModelAdminBase):
773 fields = (
774 'created',
775 'payout',
776 'file_name_link',
777 'response_code',
778 'response_text',
779 'msg_id',
780 'original_msg_id',
781 'group_status',
782 'status_reason',
783 )
784 readonly_fields = fields
785 list_display = (
786 'id',
787 'created',
788 'payout',
789 'file_name_link',
790 'response_code',
791 'response_text',
792 'original_msg_id',
793 'group_status',
794 )
796 def file_download_view(self, request, pk, filename, form_url='', extra_context=None): # pylint: disable=unused-argument
797 user = request.user
798 if not user.is_authenticated or not user.is_staff:
799 raise Http404(_('File {} not found').format(filename))
800 obj = get_object_or_404(self.get_queryset(request), pk=pk, file_name=filename)
801 assert isinstance(obj, PayoutStatus)
802 full_path = obj.full_path
803 if not os.path.isfile(full_path):
804 raise Http404(_('File {} not found').format(filename))
805 return FormattedXmlFileResponse(full_path)
807 def file_name_link(self, obj):
808 assert isinstance(obj, PayoutStatus)
809 if obj.id is None or not obj.full_path:
810 return obj.file_name
811 admin_url = reverse('admin:jbank_payoutstatus_file_download', args=(obj.id, obj.file_name,))
812 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.file_name)
813 file_name_link.short_description = _('file') # type: ignore
814 file_name_link.admin_order_field = 'file_name' # type: ignore
816 def get_urls(self):
817 urls = [
818 url(r'^(\d+)/change/status-downloads/(.+)/$', self.file_download_view, name='jbank_payoutstatus_file_download'),
819 ]
820 return urls + super().get_urls()
823class PayoutStatusInlineAdmin(admin.TabularInline):
824 model = PayoutStatus
825 can_delete = False
826 extra = 0
827 ordering = ('-id', )
828 fields = PayoutStatusAdmin.fields
829 readonly_fields = PayoutStatusAdmin.readonly_fields
831 def file_name_link(self, obj):
832 assert isinstance(obj, PayoutStatus)
833 if obj.id is None or not obj.full_path:
834 return obj.file_name
835 admin_url = reverse('admin:jbank_payoutstatus_file_download', args=(obj.id, obj.file_name,))
836 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.file_name)
837 file_name_link.short_description = _('file') # type: ignore
838 file_name_link.admin_order_field = 'file_name' # type: ignore
841class PayoutAdmin(ModelAdminBase):
842 save_on_top = False
843 exclude = ()
844 inlines = [PayoutStatusInlineAdmin]
845 date_hierarchy = 'timestamp'
847 raw_id_fields = (
848 'account',
849 'parent',
850 'payer',
851 'recipient',
852 )
854 list_filter = (
855 'state',
856 'payoutstatus_set__response_code',
857 'payoutstatus_set__group_status',
858 'recipient__bic',
859 )
861 fields = (
862 'connection',
863 'account',
864 'parent',
865 'payer',
866 'recipient',
867 'amount',
868 'messages',
869 'reference',
870 'due_date',
871 'msg_id',
872 'file_name',
873 'timestamp',
874 'paid_date',
875 'state',
876 'group_status',
877 'created',
878 )
880 list_display = (
881 'id',
882 'timestamp',
883 'recipient',
884 'amount',
885 'paid_date',
886 'state',
887 )
889 readonly_fields = (
890 'created',
891 'paid_date',
892 'timestamp',
893 'msg_id',
894 'file_name',
895 'group_status',
896 )
898 search_fields = (
899 '=msg_id',
900 '=file_name',
901 '=file_reference',
902 'recipient__name',
903 '=recipient__account_number',
904 '=msg_id',
905 '=amount',
906 )
908 def save_model(self, request, obj, form, change):
909 assert isinstance(obj, Payout)
910 if not change:
911 if not hasattr(obj, 'account') or not obj.account:
912 obj.account = obj.payer.payouts_account
913 if not hasattr(obj, 'type') or not obj.type:
914 obj.type = EntryType.objects.get(code=settings.E_BANK_PAYOUT)
915 return super().save_model(request, obj, form, change)
918class PayoutPartyAdmin(ModelAdminBase):
919 save_on_top = False
920 exclude = ()
921 search_fields = (
922 'name',
923 '=account_number',
924 '=org_id',
925 )
926 ordering = ('name',)
928 actions = (
929 )
931 list_display = (
932 'id',
933 'name',
934 'account_number',
935 'bic',
936 'org_id',
937 'address',
938 'country_code',
939 )
941 raw_id_fields = (
942 'payouts_account',
943 )
946class RefundAdmin(PayoutAdmin):
947 raw_id_fields = (
948 'account',
949 'parent',
950 'payer',
951 'recipient',
952 )
954 fields = (
955 'connection',
956 'account',
957 'payer',
958 'parent',
959 'recipient',
960 'amount',
961 'messages',
962 'reference',
963 'due_date',
964 'msg_id',
965 'file_name',
966 'timestamp',
967 'paid_date',
968 'state',
969 'group_status',
970 'created',
971 )
974class CurrencyExchangeSourceAdmin(ModelAdminBase):
975 save_on_top = False
976 exclude = ()
978 fields = (
979 'id',
980 'created',
981 'name',
982 )
984 readonly_fields = (
985 'id',
986 'created',
987 )
989 list_display = fields
992class CurrencyExchangeAdmin(ModelAdminBase):
993 save_on_top = False
995 fields = (
996 'record_date',
997 'source_currency',
998 'target_currency',
999 'unit_currency',
1000 'exchange_rate',
1001 'source',
1002 )
1004 date_hierarchy = 'record_date'
1005 readonly_fields = list_display = fields
1006 raw_id_fields = ('source', )
1007 list_filter = ('source_currency', 'target_currency', 'source')
1010class WsEdiConnectionAdmin(ModelAdminBase):
1011 save_on_top = False
1013 list_display = (
1014 'id',
1015 'created',
1016 'name',
1017 'sender_identifier',
1018 'receiver_identifier',
1019 'expires',
1020 )
1022 raw_id_fields = (
1023 )
1025 fieldsets = (
1026 (None, {
1027 'fields': [
1028 'id',
1029 'name',
1030 'enabled',
1031 'sender_identifier',
1032 'receiver_identifier',
1033 'target_identifier',
1034 'environment',
1035 'debug_commands',
1036 'created',
1037 ]
1038 }),
1039 ('PKI', {
1040 'fields': [
1041 'pki_endpoint',
1042 'pin',
1043 'bank_root_cert_file',
1044 ]
1045 }),
1046 ('EDI', {
1047 'fields': [
1048 'soap_endpoint',
1049 'signing_cert_file',
1050 'signing_key_file',
1051 'encryption_cert_file',
1052 'encryption_key_file',
1053 'bank_encryption_cert_file',
1054 'bank_signing_cert_file',
1055 'ca_cert_file',
1056 ]
1057 }),
1058 )
1060 readonly_fields = (
1061 'id',
1062 'created',
1063 'expires',
1064 )
1066 def expires(self, obj):
1067 assert isinstance(obj, WsEdiConnection)
1068 min_not_valid_after: Optional[datetime] = None
1069 try:
1070 certs = [
1071 obj.signing_cert_full_path,
1072 obj.encryption_cert_full_path,
1073 obj.bank_encryption_cert_full_path,
1074 obj.bank_root_cert_full_path,
1075 obj.ca_cert_full_path,
1076 ]
1077 except Exception as e:
1078 logger.error(e)
1079 return _('(missing certificate files)')
1080 for filename in certs:
1081 if filename and os.path.isfile(filename):
1082 cert = get_x509_cert_from_file(filename)
1083 not_valid_after = pytz.utc.localize(cert.not_valid_after)
1084 if min_not_valid_after is None or not_valid_after < min_not_valid_after:
1085 min_not_valid_after = not_valid_after
1086 return date_format(min_not_valid_after.date(), 'SHORT_DATE_FORMAT') if min_not_valid_after else ''
1087 expires.short_description = _('expires') # type: ignore
1090class WsEdiSoapCallAdmin(ModelAdminBase):
1091 save_on_top = False
1093 date_hierarchy = 'created'
1095 list_display = (
1096 'id',
1097 'created',
1098 'connection',
1099 'command',
1100 'executed',
1101 'execution_time',
1102 )
1104 list_filter = (
1105 'connection',
1106 'command',
1107 )
1109 raw_id_fields = (
1110 )
1112 fields = (
1113 'id',
1114 'connection',
1115 'command',
1116 'created',
1117 'executed',
1118 'execution_time',
1119 'error_fmt',
1120 'admin_application_request',
1121 'admin_application_response',
1122 'admin_application_response_file',
1123 )
1125 readonly_fields = (
1126 'id',
1127 'connection',
1128 'command',
1129 'created',
1130 'executed',
1131 'execution_time',
1132 'error_fmt',
1133 'admin_application_request',
1134 'admin_application_response',
1135 'admin_application_response_file',
1136 )
1138 def get_fields(self, request, obj = None):
1139 fields = super().get_fields(request, obj)
1140 if not request.user.is_superuser:
1141 fields = fields[:-2]
1142 return fields
1144 def soap_download_view(self, request, object_id, file_type, form_url = '', extra_context = None): # pylint: disable=unused-argument
1145 user = request.user
1146 if not user.is_authenticated or not user.is_superuser:
1147 raise Http404('File not found')
1148 obj = get_object_or_404(self.get_queryset(request), id=object_id)
1149 assert isinstance(obj, WsEdiSoapCall)
1150 if file_type == 'f':
1151 with open(obj.debug_response_full_path, 'rb') as fb:
1152 data = xml_to_dict(fb.read())
1153 content = base64.b64decode(data.get('Content', ''))
1154 return FormattedXmlResponse(content, filename=obj.debug_get_filename(file_type))
1155 return FormattedXmlFileResponse(WsEdiSoapCall.debug_get_file_path(obj.debug_get_filename(file_type)))
1157 def admin_application_request(self, obj):
1158 assert isinstance(obj, WsEdiSoapCall)
1159 if not os.path.isfile(obj.debug_request_full_path):
1160 return ''
1161 download_url = reverse('admin:jbank_wsedisoapcall_soap_download', args=[str(obj.id), 'a'])
1162 return mark_safe(format_html('<a href="{}">{}</a>', download_url, os.path.basename(obj.debug_request_full_path)))
1163 admin_application_request.short_description = _('application request') # type: ignore
1165 def admin_application_response(self, obj):
1166 assert isinstance(obj, WsEdiSoapCall)
1167 if not os.path.isfile(obj.debug_response_full_path):
1168 return ''
1169 download_url = reverse('admin:jbank_wsedisoapcall_soap_download', args=[str(obj.id), 'r'])
1170 return mark_safe(format_html('<a href="{}">{}</a>', download_url, os.path.basename(obj.debug_response_full_path)))
1171 admin_application_response.short_description = _('application response') # type: ignore
1173 def admin_application_response_file(self, obj):
1174 assert isinstance(obj, WsEdiSoapCall)
1175 if obj.command != 'DownloadFile' or not obj.executed:
1176 return ''
1177 file_type = 'f'
1178 download_url = reverse('admin:jbank_wsedisoapcall_soap_download', args=[str(obj.id), file_type])
1179 return mark_safe(format_html('<a href="{}">{}</a>', download_url, obj.debug_get_filename(file_type)))
1180 admin_application_response_file.short_description = _('file') # type: ignore
1182 def execution_time(self, obj):
1183 assert isinstance(obj, WsEdiSoapCall)
1184 return obj.executed - obj.created if obj.executed else ''
1185 execution_time.short_description = _('execution time') # type: ignore
1187 def error_fmt(self, obj):
1188 assert isinstance(obj, WsEdiSoapCall)
1189 return mark_safe(obj.error.replace('\n', '<br>'))
1190 error_fmt.short_description = _('error') # type: ignore
1192 def get_urls(self):
1193 info = self.model._meta.app_label, self.model._meta.model_name
1194 return [
1195 url(r'^soap-download/(\d+)/(.+)$', self.soap_download_view, name='%s_%s_soap_download' % info),
1196 ] + super().get_urls()
1199mark_as_manually_settled.short_description = _('Mark as manually settled') # type: ignore
1200unmark_manually_settled_flag.short_description = _('Unmark manually settled flag') # type: ignore
1202admin.site.register(CurrencyExchangeSource, CurrencyExchangeSourceAdmin)
1203admin.site.register(CurrencyExchange, CurrencyExchangeAdmin)
1204admin.site.register(Payout, PayoutAdmin)
1205admin.site.register(PayoutStatus, PayoutStatusAdmin)
1206admin.site.register(PayoutParty, PayoutPartyAdmin)
1207admin.site.register(Refund, RefundAdmin)
1208admin.site.register(Statement, StatementAdmin)
1209admin.site.register(StatementRecord, StatementRecordAdmin)
1210admin.site.register(StatementFile, StatementFileAdmin)
1211admin.site.register(ReferencePaymentRecord, ReferencePaymentRecordAdmin)
1212admin.site.register(ReferencePaymentBatch, ReferencePaymentBatchAdmin)
1213admin.site.register(ReferencePaymentBatchFile, ReferencePaymentBatchFileAdmin)
1214admin.site.register(WsEdiConnection, WsEdiConnectionAdmin)
1215admin.site.register(WsEdiSoapCall, WsEdiSoapCallAdmin)