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, Sequence
9import pytz
10from django import forms
11from django.conf import settings
12from django.conf.urls import url
13from django.contrib import admin
14from django.contrib import messages
15from django.contrib.admin import SimpleListFilter
16from django.contrib.auth.models import User
17from django.contrib.messages import add_message, ERROR
18from django.core.exceptions import ValidationError
19from django.core.files.uploadedfile import InMemoryUploadedFile
20from django.db import transaction
21from django.db.models import F, Q, QuerySet
22from django.db.models.aggregates import Sum
23from django.http import HttpRequest, Http404
24from django.shortcuts import render, get_object_or_404
25from django.urls import ResolverMatch, reverse
26from django.utils.formats import date_format, localize
27from django.utils.html import format_html
28from django.utils.safestring import mark_safe
29from django.utils.text import capfirst
30from django.utils.translation import gettext_lazy as _
31from jacc.admin import AccountEntryNoteInline, AccountEntryNoteAdmin
32from jacc.models import Account, EntryType, AccountEntryNote
33from jbank.x509_helpers import get_x509_cert_from_file
34from jutil.request import get_ip
35from jutil.responses import FormattedXmlResponse, FormattedXmlFileResponse
36from jutil.xml import xml_to_dict
37from jbank.helpers import create_statement, create_reference_payment_batch
38from jbank.models import (
39 Statement,
40 StatementRecord,
41 StatementRecordSepaInfo,
42 ReferencePaymentRecord,
43 ReferencePaymentBatch,
44 StatementFile,
45 ReferencePaymentBatchFile,
46 Payout,
47 Refund,
48 PayoutStatus,
49 PayoutParty,
50 StatementRecordDetail,
51 StatementRecordRemittanceInfo,
52 CurrencyExchange,
53 CurrencyExchangeSource,
54 WsEdiConnection,
55 WsEdiSoapCall,
56)
57from jbank.tito import parse_tiliote_statements_from_file, parse_tiliote_statements
58from jbank.svm import parse_svm_batches_from_file, parse_svm_batches
59from jutil.admin import ModelAdminBase, admin_log, admin_log_changed_fields
61logger = logging.getLogger(__name__)
64class BankAdminBase(ModelAdminBase):
65 def save_form(self, request, form, change):
66 if change:
67 admin_log_changed_fields(form.instance, form.changed_data, request.user, ip=get_ip(request))
68 return form.save(commit=False)
70 def save_formset(self, request, form, formset, change):
71 if formset.model == AccountEntryNote:
72 AccountEntryNoteAdmin.save_account_entry_note_formset(request, form, formset, change)
73 else:
74 formset.save()
76 @staticmethod
77 def format_admin_obj_link_list(qs: QuerySet, route: str):
78 out = ""
79 for e_id in list(qs.order_by("id").values_list("id", flat=True)):
80 if out:
81 out += " | "
82 url_path = reverse(route, args=[e_id])
83 out += f'<a href="{url_path}">id={e_id}</a>'
84 return mark_safe(out)
87class SettlementEntryTypesFilter(SimpleListFilter):
88 """
89 Filters incoming settlement type entries.
90 """
92 title = _("account entry types")
93 parameter_name = "type"
95 def lookups(self, request, model_admin):
96 choices = []
97 for e in EntryType.objects.all().filter(is_settlement=True).order_by("name"):
98 assert isinstance(e, EntryType)
99 choices.append((e.id, capfirst(e.name)))
100 return choices
102 def queryset(self, request, queryset):
103 val = self.value()
104 if val:
105 return queryset.filter(type__id=val)
106 return queryset
109class AccountEntryMatchedFilter(SimpleListFilter):
110 """
111 Filters incoming payments which do not have any child/derived account entries.
112 """
114 title = _("account.entry.matched.filter")
115 parameter_name = "matched"
117 def lookups(self, request, model_admin):
118 return [
119 ("1", capfirst(_("account.entry.not.matched"))),
120 ("2", capfirst(_("account.entry.is.matched"))),
121 ("4", capfirst(_("not marked as settled"))),
122 ("3", capfirst(_("marked as settled"))),
123 ]
125 def queryset(self, request, queryset):
126 val = self.value()
127 if val:
128 # return original settlements only
129 queryset = queryset.filter(type__is_settlement=True, parent=None)
130 if val == "1":
131 # return those which are not manually settled and
132 # have either a) no children b) sum of children less than amount
133 queryset = queryset.exclude(manually_settled=True)
134 queryset = queryset.annotate(child_set_amount=Sum("child_set__amount"))
135 return queryset.filter(Q(child_set=None) | Q(child_set_amount__lt=F("amount")))
136 if val == "2":
137 # return any entries with derived account entries or marked as manually settled
138 return queryset.exclude(Q(child_set=None) & Q(manually_settled=False))
139 if val == "3":
140 # return only manually marked as settled
141 return queryset.filter(manually_settled=True)
142 if val == "4":
143 # return everything but manually marked as settled
144 return queryset.filter(manually_settled=False)
145 return queryset
148class AccountNameFilter(SimpleListFilter):
149 """
150 Filters account entries based on account name.
151 """
153 title = _("account.name.filter")
154 parameter_name = "account-name"
156 def lookups(self, request, model_admin):
157 ops = []
158 qs = model_admin.get_queryset(request)
159 for e in qs.distinct("account__name"):
160 ops.append((e.account.name, e.account.name))
161 return sorted(ops, key=lambda x: x[0])
163 def queryset(self, request, queryset):
164 val = self.value()
165 if val:
166 return queryset.filter(account__name=val)
167 return queryset
170class StatementAdmin(BankAdminBase):
171 exclude = ()
172 list_per_page = 20
173 save_on_top = False
174 ordering = ("-record_date", "account_number")
175 date_hierarchy = "record_date"
176 list_filter = ("account_number",)
177 readonly_fields = (
178 "file_link",
179 "account_number",
180 "statement_number",
181 "begin_date",
182 "end_date",
183 "record_date",
184 "customer_identifier",
185 "begin_balance_date",
186 "begin_balance",
187 "record_count",
188 "currency_code",
189 "account_name",
190 "account_limit",
191 "owner_name",
192 "contact_info_1",
193 "contact_info_2",
194 "bank_specific_info_1",
195 "iban",
196 "bic",
197 )
198 fields = readonly_fields
199 search_fields = (
200 "name",
201 "statement_number",
202 )
203 list_display = (
204 "id",
205 "record_date_short",
206 "account_number",
207 "statement_number",
208 "begin_balance",
209 "currency_code",
210 "file_link",
211 "account_entry_list",
212 )
214 def record_date_short(self, obj):
215 return date_format(obj.record_date, "SHORT_DATE_FORMAT")
217 record_date_short.short_description = _("record date") # type: ignore
218 record_date_short.admin_order_field = "record_date" # type: ignore
220 def account_entry_list(self, obj):
221 assert isinstance(obj, Statement)
222 admin_url = reverse("admin:jbank_statementrecord_statement_changelist", args=(obj.id,))
223 return format_html(
224 "<a href='{}'>{}</a>", mark_safe(admin_url), StatementRecord.objects.filter(statement=obj).count()
225 )
227 account_entry_list.short_description = _("account entries") # type: ignore
229 def file_link(self, obj):
230 assert isinstance(obj, Statement)
231 if not obj.file:
232 return ""
233 admin_url = reverse("admin:jbank_statementfile_change", args=(obj.file.id,))
234 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.name)
236 file_link.admin_order_field = "file" # type: ignore
237 file_link.short_description = _("file") # type: ignore
239 def get_urls(self):
240 return [
241 url(
242 r"^by-file/(?P<file_id>\d+)/$",
243 self.admin_site.admin_view(self.kw_changelist_view),
244 name="jbank_statement_file_changelist",
245 ),
246 ] + super().get_urls()
248 def get_queryset(self, request: HttpRequest):
249 rm = request.resolver_match
250 assert isinstance(rm, ResolverMatch)
251 qs = super().get_queryset(request)
252 file_id = rm.kwargs.get("file_id", None)
253 if file_id:
254 qs = qs.filter(file=file_id)
255 return qs
258class StatementRecordDetailInlineAdmin(admin.StackedInline):
259 exclude = ()
260 model = StatementRecordDetail
261 can_delete = False
262 extra = 0
264 fields = (
265 "batch_identifier",
266 "amount",
267 "creditor_account",
268 "creditor_account_scheme",
269 "currency_code",
270 "instructed_amount",
271 "exchange",
272 "archive_identifier",
273 "end_to_end_identifier",
274 "creditor_name",
275 "debtor_name",
276 "ultimate_debtor_name",
277 "unstructured_remittance_info",
278 "paid_date",
279 "structured_remittance_info",
280 )
281 readonly_fields = fields
282 raw_id_fields = ()
284 def structured_remittance_info(self, obj):
285 assert isinstance(obj, StatementRecordDetail)
286 lines = []
287 for rinfo in obj.remittanceinfo_set.all().order_by("id"):
288 assert isinstance(rinfo, StatementRecordRemittanceInfo)
289 lines.append(str(rinfo))
290 return mark_safe("<br>".join(lines))
292 structured_remittance_info.short_description = _("structured remittance info") # type: ignore
294 def has_add_permission(self, request, obj=None):
295 return False
298class StatementRecordSepaInfoInlineAdmin(admin.StackedInline):
299 exclude = ()
300 model = StatementRecordSepaInfo
301 can_delete = False
302 extra = 0
303 max_num = 1
305 readonly_fields = (
306 "record",
307 "reference",
308 "iban_account_number",
309 "bic_code",
310 "recipient_name_detail",
311 "payer_name_detail",
312 "identifier",
313 "archive_identifier",
314 )
315 raw_id_fields = ("record",)
317 def has_add_permission(self, request, obj=None): # pylint: disable=unused-argument
318 return False
321def mark_as_manually_settled(modeladmin, request, qs): # pylint: disable=unused-argument
322 try:
323 data = request.POST.dict()
325 if "description" in data:
326 description = data["description"]
327 user = request.user
328 for e in list(qs.filter(manually_settled=False)):
329 e.manually_settled = True
330 e.save(update_fields=["manually_settled"])
331 msg = "{}: {}".format(capfirst(_("marked as manually settled")), description)
332 admin_log([e], msg, who=user)
333 messages.info(request, msg)
334 else:
335 cx = {
336 "qs": qs,
337 }
338 return render(request, "admin/jbank/mark_as_manually_settled.html", context=cx)
339 except ValidationError as e:
340 messages.error(request, " ".join(e.messages))
341 except Exception as e:
342 logger.error("mark_as_manually_settled: %s", traceback.format_exc())
343 messages.error(request, "{}".format(e))
344 return None
347def unmark_manually_settled_flag(modeladmin, request, qs): # pylint: disable=unused-argument
348 user = request.user
349 for e in list(qs.filter(manually_settled=True)):
350 e.manually_settled = False
351 e.save(update_fields=["manually_settled"])
352 msg = capfirst(_("manually settled flag cleared"))
353 admin_log([e], msg, who=user)
354 messages.info(request, msg)
357class StatementRecordAdmin(BankAdminBase):
358 list_per_page = 25
359 save_on_top = False
360 date_hierarchy = "record_date"
361 fields = (
362 "id",
363 "entry_type",
364 "statement",
365 "line_number",
366 "file_link",
367 "record_number",
368 "archive_identifier",
369 "record_date",
370 "value_date",
371 "paid_date",
372 "type",
373 "record_code",
374 "record_domain",
375 "family_code",
376 "sub_family_code",
377 "record_description",
378 "amount",
379 "receipt_code",
380 "delivery_method",
381 "name",
382 "name_source",
383 "recipient_account_number",
384 "recipient_account_number_changed",
385 "remittance_info",
386 "messages",
387 "client_messages",
388 "bank_messages",
389 "account",
390 "timestamp",
391 "created",
392 "last_modified",
393 "description",
394 "source_file",
395 "archived",
396 "manually_settled",
397 "is_settled_bool",
398 "child_links",
399 )
400 readonly_fields = fields
401 raw_id_fields = (
402 "statement",
403 # from AccountEntry
404 "account",
405 "source_file",
406 "parent",
407 "source_invoice",
408 "settled_invoice",
409 "settled_item",
410 )
411 list_filter = (
412 "statement__file__tag",
413 AccountNameFilter,
414 "manually_settled",
415 SettlementEntryTypesFilter,
416 "record_code",
417 )
418 search_fields = (
419 "=archive_identifier",
420 "=amount",
421 "=recipient_account_number",
422 "record_description",
423 "name",
424 "remittance_info",
425 "messages",
426 )
427 list_display = (
428 "id",
429 "record_date_short",
430 "type",
431 "record_code",
432 "amount",
433 "name",
434 "source_file_link",
435 "is_settled_bool",
436 )
437 inlines = (
438 StatementRecordSepaInfoInlineAdmin,
439 StatementRecordDetailInlineAdmin,
440 AccountEntryNoteInline,
441 )
442 actions = (
443 mark_as_manually_settled,
444 unmark_manually_settled_flag,
445 )
447 def is_settled_bool(self, obj):
448 return obj.is_settled
450 is_settled_bool.short_description = _("settled") # type: ignore
451 is_settled_bool.boolean = True # type: ignore
453 def record_date_short(self, obj):
454 return date_format(obj.record_date, "SHORT_DATE_FORMAT")
456 record_date_short.short_description = _("record date") # type: ignore
457 record_date_short.admin_order_field = "record_date" # type: ignore
459 def child_links(self, obj) -> str:
460 assert isinstance(obj, StatementRecord)
461 return self.format_admin_obj_link_list(obj.child_set, "admin:jacc_accountentry_change")
463 child_links.short_description = _("derived entries") # type: ignore
465 def get_urls(self):
466 return [
467 url(
468 r"^by-statement/(?P<statement_id>\d+)/$",
469 self.admin_site.admin_view(self.kw_changelist_view),
470 name="jbank_statementrecord_statement_changelist",
471 ),
472 url(
473 r"^by-statement-file/(?P<statement_file_id>\d+)/$",
474 self.admin_site.admin_view(self.kw_changelist_view),
475 name="jbank_statementrecord_statementfile_changelist",
476 ),
477 ] + super().get_urls()
479 def get_queryset(self, request: HttpRequest):
480 rm = request.resolver_match
481 assert isinstance(rm, ResolverMatch)
482 qs = super().get_queryset(request)
483 statement_id = rm.kwargs.get("statement_id", None)
484 if statement_id:
485 qs = qs.filter(statement__id=statement_id)
486 statement_file_id = rm.kwargs.get("statement_file_id", None)
487 if statement_file_id:
488 qs = qs.filter(statement__file_id=statement_file_id)
489 return qs
491 def source_file_link(self, obj):
492 assert isinstance(obj, StatementRecord)
493 if not obj.statement:
494 return ""
495 admin_url = reverse("admin:jbank_statementfile_change", args=(obj.statement.file.id,))
496 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.statement.name)
498 source_file_link.admin_order_field = "statement" # type: ignore
499 source_file_link.short_description = _("source file") # type: ignore
501 def file_link(self, obj):
502 assert isinstance(obj, StatementRecord)
503 if not obj.statement or not obj.statement.file:
504 return ""
505 name = basename(obj.statement.file.file.name)
506 admin_url = reverse("admin:jbank_statementfile_change", args=(obj.statement.file.id,))
507 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), name)
509 file_link.admin_order_field = "file" # type: ignore
510 file_link.short_description = _("account statement file") # type: ignore
513class ReferencePaymentRecordAdmin(BankAdminBase):
514 exclude = ()
515 list_per_page = 25
516 save_on_top = False
517 date_hierarchy = "record_date"
518 raw_id_fields = (
519 "batch",
520 # from AccountEntry
521 "account",
522 "source_file",
523 "parent",
524 "source_invoice",
525 "settled_invoice",
526 "settled_item",
527 )
528 fields = [
529 "id",
530 "batch",
531 "line_number",
532 "file_link",
533 "record_type",
534 "account_number",
535 "record_date",
536 "paid_date",
537 "archive_identifier",
538 "remittance_info",
539 "payer_name",
540 "currency_identifier",
541 "name_source",
542 "amount",
543 "correction_identifier",
544 "delivery_method",
545 "receipt_code",
546 "archived",
547 "account",
548 "created",
549 "last_modified",
550 "timestamp",
551 "type",
552 "description",
553 "manually_settled",
554 "is_settled_bool",
555 "child_links",
556 ]
557 readonly_fields = (
558 "id",
559 "batch",
560 "line_number",
561 "file_link",
562 "record_type",
563 "account_number",
564 "record_date",
565 "paid_date",
566 "archive_identifier",
567 "remittance_info",
568 "payer_name",
569 "currency_identifier",
570 "name_source",
571 "amount",
572 "correction_identifier",
573 "delivery_method",
574 "receipt_code",
575 "archived",
576 "manually_settled",
577 "account",
578 "timestamp",
579 "created",
580 "last_modified",
581 "type",
582 "description",
583 "amount",
584 "source_file",
585 "source_invoice",
586 "settled_invoice",
587 "settled_item",
588 "parent",
589 "is_settled_bool",
590 "child_links",
591 )
592 list_filter = (
593 "batch__file__tag",
594 AccountNameFilter,
595 AccountEntryMatchedFilter,
596 "correction_identifier",
597 )
598 search_fields = (
599 "=archive_identifier",
600 "=amount",
601 "remittance_info",
602 "payer_name",
603 "batch__name",
604 )
605 list_display = (
606 "id",
607 "record_date",
608 "type",
609 "amount",
610 "payer_name",
611 "remittance_info",
612 "source_file_link",
613 "is_settled_bool",
614 )
615 actions = (
616 mark_as_manually_settled,
617 unmark_manually_settled_flag,
618 )
619 inlines = [
620 AccountEntryNoteInline,
621 ]
623 def is_settled_bool(self, obj):
624 return obj.is_settled
626 is_settled_bool.short_description = _("settled") # type: ignore
627 is_settled_bool.boolean = True # type: ignore
629 def record_date_short(self, obj):
630 return date_format(obj.record_date, "SHORT_DATE_FORMAT")
632 record_date_short.short_description = _("record date") # type: ignore
633 record_date_short.admin_order_field = "record_date" # type: ignore
635 def child_links(self, obj) -> str:
636 assert isinstance(obj, ReferencePaymentRecord)
637 return self.format_admin_obj_link_list(obj.child_set, "admin:jacc_accountentry_change")
639 child_links.short_description = _("derived entries") # type: ignore
641 def file_link(self, obj):
642 assert isinstance(obj, ReferencePaymentRecord)
643 if not obj.batch or not obj.batch.file:
644 return ""
645 admin_url = reverse("admin:jbank_referencepaymentbatchfile_change", args=(obj.batch.file.id,))
646 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.batch.file)
648 file_link.admin_order_field = "file" # type: ignore
649 file_link.short_description = _("file") # type: ignore
651 def get_urls(self):
652 return [
653 url(
654 r"^by-batch/(?P<batch_id>\d+)/$",
655 self.admin_site.admin_view(self.kw_changelist_view),
656 name="jbank_referencepaymentrecord_batch_changelist",
657 ),
658 url(
659 r"^by-statement-file/(?P<stm_id>\d+)/$",
660 self.admin_site.admin_view(self.kw_changelist_view),
661 name="jbank_referencepaymentrecord_statementfile_changelist",
662 ),
663 ] + super().get_urls()
665 def get_queryset(self, request: HttpRequest):
666 rm = request.resolver_match
667 assert isinstance(rm, ResolverMatch)
668 qs = super().get_queryset(request)
669 batch_id = rm.kwargs.get("batch_id", None)
670 if batch_id:
671 qs = qs.filter(batch_id=batch_id)
672 stm_id = rm.kwargs.get("stm_id", None)
673 if stm_id:
674 qs = qs.filter(batch__file_id=stm_id)
675 return qs
677 def source_file_link(self, obj):
678 assert isinstance(obj, ReferencePaymentRecord)
679 if not obj.batch:
680 return ""
681 admin_url = reverse("admin:jbank_referencepaymentbatchfile_change", args=(obj.batch.file.id,))
682 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.batch.name)
684 source_file_link.admin_order_field = "batch" # type: ignore
685 source_file_link.short_description = _("source file") # type: ignore
688class ReferencePaymentBatchAdmin(BankAdminBase):
689 exclude = ()
690 list_per_page = 20
691 save_on_top = False
692 ordering = ("-record_date",)
693 date_hierarchy = "record_date"
694 list_filter = ("record_set__account_number",)
695 fields = (
696 "file_link",
697 "record_date",
698 "institution_identifier",
699 "service_identifier",
700 "currency_identifier",
701 )
702 readonly_fields = (
703 "name",
704 "file",
705 "file_link",
706 "record_date",
707 "institution_identifier",
708 "service_identifier",
709 "currency_identifier",
710 )
711 search_fields = (
712 "name",
713 "=record_set__archive_identifier",
714 "=record_set__amount",
715 "record_set__remittance_info",
716 "record_set__payer_name",
717 )
718 list_display = (
719 "id",
720 "name",
721 "record_date_short",
722 "service_identifier",
723 "currency_identifier",
724 "account_entry_list",
725 )
727 def record_date_short(self, obj):
728 return date_format(obj.record_date, "SHORT_DATE_FORMAT")
730 record_date_short.short_description = _("record date") # type: ignore
731 record_date_short.admin_order_field = "record_date" # type: ignore
733 def account_entry_list(self, obj):
734 assert isinstance(obj, ReferencePaymentBatch)
735 admin_url = reverse("admin:jbank_referencepaymentrecord_batch_changelist", args=(obj.id,))
736 return format_html(
737 "<a href='{}'>{}</a>", mark_safe(admin_url), ReferencePaymentRecord.objects.filter(batch=obj).count()
738 )
740 account_entry_list.short_description = _("account entries") # type: ignore
742 def file_link(self, obj):
743 assert isinstance(obj, ReferencePaymentBatch)
744 if not obj.file:
745 return ""
746 admin_url = reverse("admin:jbank_referencepaymentbatchfile_change", args=(obj.file.id,))
747 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.file)
749 file_link.admin_order_field = "file" # type: ignore
750 file_link.short_description = _("file") # type: ignore
752 def get_urls(self):
753 return [
754 url(
755 r"^by-file/(?P<file_id>\d+)/$",
756 self.admin_site.admin_view(self.kw_changelist_view),
757 name="jbank_referencepaymentbatch_file_changelist",
758 ),
759 ] + super().get_urls()
761 def get_queryset(self, request: HttpRequest):
762 rm = request.resolver_match
763 assert isinstance(rm, ResolverMatch)
764 qs = super().get_queryset(request)
765 file_id = rm.kwargs.get("file_id", None)
766 if file_id:
767 qs = qs.filter(file=file_id)
768 return qs
771class StatementFileForm(forms.ModelForm):
772 class Meta:
773 fields = [
774 "file",
775 ]
777 def clean_file(self):
778 file = self.cleaned_data["file"]
779 assert isinstance(file, InMemoryUploadedFile)
780 name = file.name
781 file.seek(0)
782 content = file.read()
783 assert isinstance(content, bytes)
784 try:
785 statements = parse_tiliote_statements(content.decode("ISO-8859-1"), filename=basename(name))
786 for stm in statements:
787 account_number = stm["header"]["account_number"]
788 if Account.objects.filter(name=account_number).count() == 0:
789 raise ValidationError(_("account.not.found").format(account_number=account_number))
790 except ValidationError:
791 raise
792 except Exception as e:
793 raise ValidationError(_("Unhandled error") + ": {}".format(e))
794 return file
797class StatementFileAdmin(BankAdminBase):
798 save_on_top = False
799 exclude = ()
800 form = StatementFileForm
802 date_hierarchy = "created"
804 search_fields = ("original_filename__contains",)
806 list_filter = ("tag",)
808 list_display = (
809 "id",
810 "created",
811 "file",
812 "records",
813 )
815 readonly_fields = (
816 "created",
817 "errors",
818 "file",
819 "original_filename",
820 "records",
821 )
823 def records(self, obj):
824 assert isinstance(obj, StatementFile)
825 url = reverse("admin:jbank_statementrecord_statementfile_changelist", args=[obj.id])
826 return format_html('<a href="{}">{}</a>', url, _("statement records"))
828 records.short_description = _("statement records") # type: ignore
830 def has_add_permission(self, request: HttpRequest) -> bool:
831 return False
833 def construct_change_message(self, request, form, formsets, add=False):
834 if add:
835 instance = form.instance
836 assert isinstance(instance, StatementFile)
837 if instance.file:
838 full_path = instance.full_path
839 plain_filename = basename(full_path)
840 try:
841 statements = parse_tiliote_statements_from_file(full_path)
842 with transaction.atomic():
843 for data in statements:
844 create_statement(data, name=plain_filename, file=instance)
845 except Exception as e:
846 instance.errors = traceback.format_exc()
847 instance.save()
848 add_message(request, ERROR, str(e))
849 instance.delete()
851 return super().construct_change_message(request, form, formsets, add)
854class ReferencePaymentBatchFileForm(forms.ModelForm):
855 class Meta:
856 fields = [
857 "file",
858 ]
860 def clean_file(self):
861 file = self.cleaned_data["file"]
862 assert isinstance(file, InMemoryUploadedFile)
863 name = file.name
864 file.seek(0)
865 content = file.read()
866 assert isinstance(content, bytes)
867 try:
868 batches = parse_svm_batches(content.decode("ISO-8859-1"), filename=basename(name))
869 for b in batches:
870 for rec in b["records"]:
871 account_number = rec["account_number"]
872 if Account.objects.filter(name=account_number).count() == 0:
873 raise ValidationError(_("account.not.found").format(account_number=account_number))
874 except ValidationError:
875 raise
876 except Exception as e:
877 raise ValidationError(_("Unhandled error") + ": {}".format(e))
878 return file
881class ReferencePaymentBatchFileAdmin(BankAdminBase):
882 save_on_top = False
883 exclude = ()
884 form = ReferencePaymentBatchFileForm
885 date_hierarchy = "created"
887 list_display = (
888 "id",
889 "created",
890 "file",
891 "total",
892 )
894 list_filter = ("tag",)
896 search_fields = ("file__contains",)
898 readonly_fields = (
899 "created",
900 "errors",
901 "file",
902 "original_filename",
903 )
905 def has_add_permission(self, request: HttpRequest) -> bool:
906 return False
908 def total(self, obj):
909 assert isinstance(obj, ReferencePaymentBatchFile)
910 path = reverse("admin:jbank_referencepaymentrecord_statementfile_changelist", args=[obj.id])
911 return format_html('<a href="{}">{}</a>', path, localize(obj.total_amount))
913 total.short_description = _("total amount") # type: ignore
915 def construct_change_message(self, request, form, formsets, add=False):
916 if add:
917 instance = form.instance
918 assert isinstance(instance, ReferencePaymentBatchFile)
919 if instance.file:
920 full_path = instance.full_path
921 plain_filename = basename(full_path)
922 try:
923 batches = parse_svm_batches_from_file(full_path)
924 with transaction.atomic():
925 for data in batches:
926 create_reference_payment_batch(data, name=plain_filename, file=instance)
927 except Exception as e:
928 user = request.user
929 assert isinstance(user, User)
930 instance.errors = traceback.format_exc()
931 instance.save()
932 msg = str(e)
933 if user.is_superuser:
934 msg = instance.errors
935 logger.error("%s: %s", plain_filename, msg)
936 add_message(request, ERROR, msg)
937 instance.delete()
939 return super().construct_change_message(request, form, formsets, add)
942class PayoutStatusAdmin(BankAdminBase):
943 fields = (
944 "created",
945 "payout",
946 "file_name_link",
947 "response_code",
948 "response_text",
949 "msg_id",
950 "original_msg_id",
951 "group_status",
952 "status_reason",
953 )
954 readonly_fields = fields
955 list_display = (
956 "id",
957 "created",
958 "payout",
959 "file_name_link",
960 "response_code",
961 "response_text",
962 "original_msg_id",
963 "group_status",
964 )
966 def file_download_view(
967 self, request, pk, filename, form_url="", extra_context=None
968 ): # pylint: disable=unused-argument
969 user = request.user
970 if not user.is_authenticated or not user.is_staff:
971 raise Http404(_("File {} not found").format(filename))
972 obj = get_object_or_404(self.get_queryset(request), pk=pk, file_name=filename)
973 assert isinstance(obj, PayoutStatus)
974 full_path = obj.full_path
975 if not os.path.isfile(full_path):
976 raise Http404(_("File {} not found").format(filename))
977 return FormattedXmlFileResponse(full_path)
979 def file_name_link(self, obj):
980 assert isinstance(obj, PayoutStatus)
981 if obj.id is None or not obj.full_path:
982 return obj.file_name
983 admin_url = reverse(
984 "admin:jbank_payoutstatus_file_download",
985 args=(
986 obj.id,
987 obj.file_name,
988 ),
989 )
990 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.file_name)
992 file_name_link.short_description = _("file") # type: ignore
993 file_name_link.admin_order_field = "file_name" # type: ignore
995 def get_urls(self):
996 urls = [
997 url(
998 r"^(\d+)/change/status-downloads/(.+)/$",
999 self.file_download_view,
1000 name="jbank_payoutstatus_file_download",
1001 ),
1002 ]
1003 return urls + super().get_urls()
1006class PayoutStatusInlineAdmin(admin.TabularInline):
1007 model = PayoutStatus
1008 can_delete = False
1009 extra = 0
1010 ordering = ("-id",)
1011 fields = PayoutStatusAdmin.fields
1012 readonly_fields = PayoutStatusAdmin.readonly_fields
1014 def file_name_link(self, obj):
1015 assert isinstance(obj, PayoutStatus)
1016 if obj.id is None or not obj.full_path:
1017 return obj.file_name
1018 admin_url = reverse(
1019 "admin:jbank_payoutstatus_file_download",
1020 args=(
1021 obj.id,
1022 obj.file_name,
1023 ),
1024 )
1025 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.file_name)
1027 file_name_link.short_description = _("file") # type: ignore
1028 file_name_link.admin_order_field = "file_name" # type: ignore
1031class PayoutAdmin(BankAdminBase):
1032 save_on_top = False
1033 exclude = ()
1034 inlines = [PayoutStatusInlineAdmin, AccountEntryNoteInline]
1035 date_hierarchy = "timestamp"
1037 raw_id_fields: Sequence[str] = (
1038 "account",
1039 "parent",
1040 "payer",
1041 "recipient",
1042 )
1044 list_filter: Sequence[str] = (
1045 "state",
1046 "payoutstatus_set__response_code",
1047 "payoutstatus_set__group_status",
1048 "recipient__bic",
1049 )
1051 fields: Sequence[str] = (
1052 "connection",
1053 "account",
1054 "parent",
1055 "payer",
1056 "recipient",
1057 "amount",
1058 "messages",
1059 "reference",
1060 "due_date",
1061 "msg_id",
1062 "file_name",
1063 "timestamp",
1064 "paid_date",
1065 "state",
1066 "group_status",
1067 "created",
1068 )
1070 list_display: Sequence[str] = (
1071 "id",
1072 "timestamp",
1073 "recipient",
1074 "amount",
1075 "paid_date",
1076 "state",
1077 )
1079 readonly_fields: Sequence[str] = (
1080 "created",
1081 "paid_date",
1082 "timestamp",
1083 "msg_id",
1084 "file_name",
1085 "group_status",
1086 )
1088 search_fields: Sequence[str] = (
1089 "=msg_id",
1090 "=file_name",
1091 "=file_reference",
1092 "recipient__name",
1093 "=recipient__account_number",
1094 "=msg_id",
1095 "=amount",
1096 )
1098 def save_model(self, request, obj, form, change):
1099 assert isinstance(obj, Payout)
1100 if not change:
1101 if not hasattr(obj, "account") or not obj.account:
1102 obj.account = obj.payer.payouts_account
1103 if not hasattr(obj, "type") or not obj.type:
1104 obj.type = EntryType.objects.get(code=settings.E_BANK_PAYOUT)
1105 return super().save_model(request, obj, form, change)
1108class PayoutPartyAdmin(BankAdminBase):
1109 save_on_top = False
1110 exclude = ()
1111 search_fields = (
1112 "name",
1113 "=account_number",
1114 "=org_id",
1115 )
1116 ordering = ("name",)
1118 actions = ()
1120 list_display = (
1121 "id",
1122 "name",
1123 "account_number",
1124 "bic",
1125 "org_id",
1126 "address",
1127 "country_code",
1128 )
1130 raw_id_fields = ("payouts_account",)
1133class RefundAdmin(PayoutAdmin):
1134 raw_id_fields = (
1135 "account",
1136 "parent",
1137 "payer",
1138 "recipient",
1139 )
1140 fields = (
1141 "connection",
1142 "account",
1143 "payer",
1144 "parent",
1145 "recipient",
1146 "amount",
1147 "messages",
1148 "reference",
1149 "attachment",
1150 "msg_id",
1151 "file_name",
1152 "timestamp",
1153 "paid_date",
1154 "group_status",
1155 "created",
1156 )
1157 readonly_fields = (
1158 "msg_id",
1159 "file_name",
1160 "timestamp",
1161 "paid_date",
1162 "group_status",
1163 "created",
1164 )
1165 inlines = [AccountEntryNoteInline]
1168class CurrencyExchangeSourceAdmin(BankAdminBase):
1169 save_on_top = False
1170 exclude = ()
1172 fields = (
1173 "id",
1174 "created",
1175 "name",
1176 )
1178 readonly_fields = (
1179 "id",
1180 "created",
1181 )
1183 list_display = fields
1186class CurrencyExchangeAdmin(BankAdminBase):
1187 save_on_top = False
1189 fields = (
1190 "record_date",
1191 "source_currency",
1192 "target_currency",
1193 "unit_currency",
1194 "exchange_rate",
1195 "source",
1196 )
1198 date_hierarchy = "record_date"
1199 readonly_fields = list_display = fields
1200 raw_id_fields = ("source",)
1201 list_filter = ("source_currency", "target_currency", "source")
1204class WsEdiConnectionAdmin(BankAdminBase):
1205 save_on_top = False
1207 ordering = [
1208 "name",
1209 ]
1211 list_display = (
1212 "id",
1213 "created",
1214 "name",
1215 "sender_identifier",
1216 "receiver_identifier",
1217 "expires",
1218 )
1220 raw_id_fields = ()
1222 fieldsets = (
1223 (
1224 None,
1225 {
1226 "fields": [
1227 "id",
1228 "name",
1229 "enabled",
1230 "sender_identifier",
1231 "receiver_identifier",
1232 "target_identifier",
1233 "environment",
1234 "debug_commands",
1235 "created",
1236 ]
1237 },
1238 ),
1239 (
1240 "PKI",
1241 {
1242 "fields": [
1243 "pki_endpoint",
1244 "pin",
1245 "bank_root_cert_file",
1246 ]
1247 },
1248 ),
1249 (
1250 "EDI",
1251 {
1252 "fields": [
1253 "soap_endpoint",
1254 "signing_cert_file",
1255 "signing_key_file",
1256 "encryption_cert_file",
1257 "encryption_key_file",
1258 "bank_encryption_cert_file",
1259 "bank_signing_cert_file",
1260 "ca_cert_file",
1261 ]
1262 },
1263 ),
1264 )
1266 readonly_fields = (
1267 "id",
1268 "created",
1269 "expires",
1270 )
1272 def expires(self, obj):
1273 assert isinstance(obj, WsEdiConnection)
1274 min_not_valid_after: Optional[datetime] = None
1275 try:
1276 certs = [
1277 obj.signing_cert_full_path,
1278 obj.encryption_cert_full_path,
1279 obj.bank_encryption_cert_full_path,
1280 obj.bank_root_cert_full_path,
1281 obj.ca_cert_full_path,
1282 ]
1283 except Exception as e:
1284 logger.error(e)
1285 return _("(missing certificate files)")
1286 for filename in certs:
1287 if filename and os.path.isfile(filename):
1288 cert = get_x509_cert_from_file(filename)
1289 not_valid_after = pytz.utc.localize(cert.not_valid_after)
1290 if min_not_valid_after is None or not_valid_after < min_not_valid_after:
1291 min_not_valid_after = not_valid_after
1292 return date_format(min_not_valid_after.date(), "SHORT_DATE_FORMAT") if min_not_valid_after else ""
1294 expires.short_description = _("expires") # type: ignore
1297class WsEdiSoapCallAdmin(BankAdminBase):
1298 save_on_top = False
1300 date_hierarchy = "created"
1302 list_display = (
1303 "id",
1304 "created",
1305 "connection",
1306 "command",
1307 "executed",
1308 "execution_time",
1309 )
1311 list_filter = (
1312 "connection",
1313 "command",
1314 )
1316 raw_id_fields = ()
1318 fields = (
1319 "id",
1320 "connection",
1321 "command",
1322 "created",
1323 "executed",
1324 "execution_time",
1325 "error_fmt",
1326 "admin_application_request",
1327 "admin_application_response",
1328 "admin_application_response_file",
1329 )
1331 readonly_fields = (
1332 "id",
1333 "connection",
1334 "command",
1335 "created",
1336 "executed",
1337 "execution_time",
1338 "error_fmt",
1339 "admin_application_request",
1340 "admin_application_response",
1341 "admin_application_response_file",
1342 )
1344 def get_fields(self, request, obj=None):
1345 fields = super().get_fields(request, obj)
1346 if not request.user.is_superuser:
1347 fields = fields[:-2]
1348 return fields
1350 def soap_download_view(
1351 self, request, object_id, file_type, form_url="", extra_context=None
1352 ): # pylint: disable=unused-argument
1353 user = request.user
1354 if not user.is_authenticated or not user.is_superuser:
1355 raise Http404("File not found")
1356 obj = get_object_or_404(self.get_queryset(request), id=object_id)
1357 assert isinstance(obj, WsEdiSoapCall)
1358 if file_type == "f":
1359 with open(obj.debug_response_full_path, "rb") as fb:
1360 data = xml_to_dict(fb.read())
1361 content = base64.b64decode(data.get("Content", ""))
1362 return FormattedXmlResponse(content, filename=obj.debug_get_filename(file_type))
1363 return FormattedXmlFileResponse(WsEdiSoapCall.debug_get_file_path(obj.debug_get_filename(file_type)))
1365 def admin_application_request(self, obj):
1366 assert isinstance(obj, WsEdiSoapCall)
1367 if not os.path.isfile(obj.debug_request_full_path):
1368 return ""
1369 download_url = reverse("admin:jbank_wsedisoapcall_soap_download", args=[str(obj.id), "q"])
1370 return mark_safe(
1371 format_html('<a href="{}">{}</a>', download_url, os.path.basename(obj.debug_request_full_path))
1372 )
1374 admin_application_request.short_description = _("application request") # type: ignore
1376 def admin_application_response(self, obj):
1377 assert isinstance(obj, WsEdiSoapCall)
1378 if not os.path.isfile(obj.debug_response_full_path):
1379 return ""
1380 download_url = reverse("admin:jbank_wsedisoapcall_soap_download", args=[str(obj.id), "s"])
1381 return mark_safe(
1382 format_html('<a href="{}">{}</a>', download_url, os.path.basename(obj.debug_response_full_path))
1383 )
1385 admin_application_response.short_description = _("application response") # type: ignore
1387 def admin_application_response_file(self, obj):
1388 assert isinstance(obj, WsEdiSoapCall)
1389 if obj.command != "DownloadFile" or not obj.executed:
1390 return ""
1391 file_type = "f"
1392 download_url = reverse("admin:jbank_wsedisoapcall_soap_download", args=[str(obj.id), file_type])
1393 return mark_safe(format_html('<a href="{}">{}</a>', download_url, obj.debug_get_filename(file_type)))
1395 admin_application_response_file.short_description = _("file") # type: ignore
1397 def execution_time(self, obj):
1398 assert isinstance(obj, WsEdiSoapCall)
1399 return obj.executed - obj.created if obj.executed else ""
1401 execution_time.short_description = _("execution time") # type: ignore
1403 def error_fmt(self, obj):
1404 assert isinstance(obj, WsEdiSoapCall)
1405 return mark_safe(obj.error.replace("\n", "<br>"))
1407 error_fmt.short_description = _("error") # type: ignore
1409 def get_urls(self):
1410 info = self.model._meta.app_label, self.model._meta.model_name
1411 return [
1412 url(r"^soap-download/(\d+)/(.+)$", self.soap_download_view, name="%s_%s_soap_download" % info),
1413 ] + super().get_urls()
1416mark_as_manually_settled.short_description = _("Mark as manually settled") # type: ignore
1417unmark_manually_settled_flag.short_description = _("Unmark manually settled flag") # type: ignore
1419admin.site.register(CurrencyExchangeSource, CurrencyExchangeSourceAdmin)
1420admin.site.register(CurrencyExchange, CurrencyExchangeAdmin)
1421admin.site.register(Payout, PayoutAdmin)
1422admin.site.register(PayoutStatus, PayoutStatusAdmin)
1423admin.site.register(PayoutParty, PayoutPartyAdmin)
1424admin.site.register(Refund, RefundAdmin)
1425admin.site.register(Statement, StatementAdmin)
1426admin.site.register(StatementRecord, StatementRecordAdmin)
1427admin.site.register(StatementFile, StatementFileAdmin)
1428admin.site.register(ReferencePaymentRecord, ReferencePaymentRecordAdmin)
1429admin.site.register(ReferencePaymentBatch, ReferencePaymentBatchAdmin)
1430admin.site.register(ReferencePaymentBatchFile, ReferencePaymentBatchFileAdmin)
1431admin.site.register(WsEdiConnection, WsEdiConnectionAdmin)
1432admin.site.register(WsEdiSoapCall, WsEdiSoapCallAdmin)