Coverage for jbank/admin.py: 48%
794 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-27 13:36 +0700
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-27 13:36 +0700
1# pylint: disable=too-many-arguments
2import base64
3import logging
4import os
5import traceback
6from datetime import datetime
7from decimal import Decimal
8from os.path import basename
9from typing import Optional, Sequence, List
10import pytz
11from django import forms
12from django.conf import settings
13from django.contrib import admin
14from django.contrib import messages
15from django.contrib.admin import SimpleListFilter
16from django.contrib.auth.models import User # noqa
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, path, re_path, URLPattern
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.timezone import now
31from django.utils.translation import gettext_lazy as _
32from jacc.admin import AccountEntryNoteInline, AccountEntryNoteAdmin
33from jacc.helpers import sum_queryset
34from jacc.models import Account, EntryType, AccountEntryNote
35from jbank.x509_helpers import get_x509_cert_from_file
36from jutil.format import dec2, format_timedelta
37from jutil.request import get_ip
38from jutil.responses import FormattedXmlResponse, FormattedXmlFileResponse
39from jutil.validators import iban_bic
40from jutil.xml import xml_to_dict
41from jbank.models import (
42 Statement,
43 StatementRecord,
44 StatementRecordSepaInfo,
45 ReferencePaymentRecord,
46 ReferencePaymentBatch,
47 StatementFile,
48 ReferencePaymentBatchFile,
49 Payout,
50 Refund,
51 PayoutStatus,
52 PayoutParty,
53 StatementRecordDetail,
54 StatementRecordRemittanceInfo,
55 CurrencyExchange,
56 CurrencyExchangeSource,
57 WsEdiConnection,
58 WsEdiSoapCall,
59 EuriborRate,
60 PAYOUT_WAITING_PROCESSING,
61 PAYOUT_ERROR,
62 PAYOUT_PAID,
63 PAYOUT_ON_HOLD,
64 AccountBalance,
65)
66from jbank.tito import parse_tiliote_statements_from_file, parse_tiliote_statements
67from jbank.svm import parse_svm_batches_from_file, parse_svm_batches, create_statement, create_reference_payment_batch
68from jutil.admin import ModelAdminBase, admin_log
70logger = logging.getLogger(__name__)
73class BankAdminBase(ModelAdminBase):
74 save_on_top = False
76 def save_formset(self, request, form, formset, change):
77 if formset.model == AccountEntryNote:
78 AccountEntryNoteAdmin.save_account_entry_note_formset(request, form, formset, change)
79 else:
80 formset.save()
82 @staticmethod
83 def format_admin_obj_link_list(qs: QuerySet, route: str):
84 out = ""
85 for e_id in list(qs.order_by("id").values_list("id", flat=True)):
86 if out:
87 out += " | "
88 url_path = reverse(route, args=[e_id])
89 out += f'<a href="{url_path}">id={e_id}</a>'
90 return mark_safe(out)
92 @staticmethod
93 def format_currency(value: Optional[Decimal]) -> str:
94 if value is None:
95 return ""
96 value_str = localize(dec2(value))
97 out = value_str[-3] + value_str[-2:]
98 value_str = value_str[:-3]
99 while len(value_str) > 3:
100 out = " " + value_str[-3:] + out
101 value_str = value_str[:-3]
102 out = value_str + out
103 return mark_safe(out)
106class SettlementEntryTypesFilter(SimpleListFilter):
107 """
108 Filters incoming settlement type entries.
109 """
111 title = _("account entry types")
112 parameter_name = "type"
114 def lookups(self, request, model_admin):
115 choices = []
116 for e in EntryType.objects.all().filter(is_settlement=True).order_by("name"):
117 assert isinstance(e, EntryType)
118 choices.append((e.id, capfirst(e.name)))
119 return choices
121 def queryset(self, request, queryset):
122 val = self.value()
123 if val:
124 return queryset.filter(type__id=val)
125 return queryset
128class AccountEntryMatchedFilter(SimpleListFilter):
129 """
130 Filters incoming payments which do not have any child/derived account entries.
131 """
133 title = _("account.entry.matched.filter")
134 parameter_name = "matched"
136 def lookups(self, request, model_admin):
137 return [
138 ("1", capfirst(_("account.entry.not.matched"))),
139 ("2", capfirst(_("account.entry.is.matched"))),
140 ("4", capfirst(_("not marked as settled"))),
141 ("3", capfirst(_("marked as settled"))),
142 ]
144 def queryset(self, request, queryset):
145 val = self.value()
146 if val:
147 # return original settlements only
148 queryset = queryset.filter(type__is_settlement=True, parent=None)
149 if val == "1":
150 # return those which are not manually settled and
151 # have either a) no children b) sum of children less than amount
152 queryset = queryset.exclude(manually_settled=True)
153 queryset = queryset.annotate(child_set_amount=Sum("child_set__amount"))
154 return queryset.filter(Q(child_set=None) | Q(child_set_amount__lt=F("amount")))
155 if val == "2":
156 # return any entries with derived account entries or marked as manually settled
157 return queryset.exclude(Q(child_set=None) & Q(manually_settled=False))
158 if val == "3":
159 # return only manually marked as settled
160 return queryset.filter(manually_settled=True)
161 if val == "4":
162 # return everything but manually marked as settled
163 return queryset.filter(manually_settled=False)
164 return queryset
167class AccountNameFilter(SimpleListFilter):
168 """
169 Filters account entries based on account name.
170 """
172 title = _("account.name.filter")
173 parameter_name = "account-name"
175 def lookups(self, request, model_admin):
176 ops = []
177 qs = model_admin.get_queryset(request)
178 for e in qs.distinct("account__name"):
179 ops.append((e.account.name, e.account.name))
180 return sorted(ops, key=lambda x: x[0])
182 def queryset(self, request, queryset):
183 val = self.value()
184 if val:
185 return queryset.filter(account__name=val)
186 return queryset
189class StatementAdmin(BankAdminBase):
190 exclude = ()
191 list_per_page = 20
192 save_on_top = False
193 ordering = ("-record_date", "account_number")
194 date_hierarchy = "end_date"
195 list_filter = ("account_number",)
196 readonly_fields = (
197 "file_link",
198 "account_number",
199 "bic_code",
200 "statement_number",
201 "begin_date",
202 "end_date",
203 "record_date",
204 "customer_identifier",
205 "begin_balance_date",
206 "begin_balance",
207 "record_count",
208 "currency_code",
209 "account_name",
210 "account_limit",
211 "owner_name",
212 "contact_info_1",
213 "contact_info_2",
214 "bank_specific_info_1",
215 "iban",
216 "bic",
217 )
218 fields = readonly_fields
219 search_fields = (
220 "name",
221 "statement_number",
222 )
223 list_display = (
224 "id",
225 "statement_date_short",
226 "account_number",
227 "bic_code",
228 "statement_number",
229 "begin_balance",
230 "currency_code",
231 "file_link",
232 "account_entry_list",
233 )
235 def bic_code(self, obj):
236 assert isinstance(obj, Statement)
237 return iban_bic(obj.account_number)
239 bic_code.short_description = "BIC" # type: ignore
241 def statement_date_short(self, obj):
242 return date_format(obj.end_date, "SHORT_DATE_FORMAT")
244 statement_date_short.short_description = _("date") # type: ignore
245 statement_date_short.admin_order_field = "end_date" # type: ignore
247 def record_date_short(self, obj):
248 return date_format(obj.record_date, "SHORT_DATE_FORMAT")
250 record_date_short.short_description = _("record date") # type: ignore
251 record_date_short.admin_order_field = "record_date" # type: ignore
253 def account_entry_list(self, obj):
254 assert isinstance(obj, Statement)
255 admin_url = reverse("admin:jbank_statementrecord_statement_changelist", args=(obj.id,))
256 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), StatementRecord.objects.filter(statement=obj).count())
258 account_entry_list.short_description = _("account entries") # type: ignore
260 def file_link(self, obj):
261 assert isinstance(obj, Statement)
262 if not obj.file:
263 return ""
264 admin_url = reverse("admin:jbank_statementfile_change", args=(obj.file.id,))
265 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.name)
267 file_link.admin_order_field = "file" # type: ignore
268 file_link.short_description = _("file") # type: ignore
270 def get_urls(self):
271 return [
272 path(
273 "by-file/<int:file_id>/",
274 self.admin_site.admin_view(self.kw_changelist_view),
275 name="jbank_statement_file_changelist",
276 ),
277 ] + super().get_urls()
279 def get_queryset(self, request: HttpRequest):
280 rm = request.resolver_match
281 assert isinstance(rm, ResolverMatch)
282 qs = super().get_queryset(request)
283 file_id = rm.kwargs.get("file_id", None)
284 if file_id:
285 qs = qs.filter(file=file_id)
286 return qs
289class StatementRecordDetailInlineAdmin(admin.StackedInline):
290 exclude = ()
291 model = StatementRecordDetail
292 can_delete = False
293 extra = 0
295 fields = (
296 "batch_identifier",
297 "amount",
298 "creditor_account",
299 "creditor_account_scheme",
300 "currency_code",
301 "instructed_amount",
302 "exchange",
303 "archive_identifier",
304 "end_to_end_identifier",
305 "creditor_name",
306 "debtor_name",
307 "ultimate_debtor_name",
308 "unstructured_remittance_info",
309 "paid_date",
310 "structured_remittance_info",
311 )
312 readonly_fields = fields
313 raw_id_fields = ()
315 def structured_remittance_info(self, obj):
316 assert isinstance(obj, StatementRecordDetail)
317 lines = []
318 for rinfo in obj.remittanceinfo_set.all().order_by("id"):
319 assert isinstance(rinfo, StatementRecordRemittanceInfo)
320 lines.append(str(rinfo))
321 return mark_safe("<br>".join(lines))
323 structured_remittance_info.short_description = _("structured remittance info") # type: ignore
325 def has_add_permission(self, request, obj=None):
326 return False
329class StatementRecordSepaInfoInlineAdmin(admin.StackedInline):
330 exclude = ()
331 model = StatementRecordSepaInfo
332 can_delete = False
333 extra = 0
334 max_num = 1
336 readonly_fields = (
337 "record",
338 "reference",
339 "iban_account_number",
340 "bic_code",
341 "recipient_name_detail",
342 "payer_name_detail",
343 "identifier",
344 "archive_identifier",
345 )
346 raw_id_fields = ("record",)
348 def has_add_permission(self, request, obj=None):
349 return False
352def mark_as_manually_settled(modeladmin, request, qs): # pylint: disable=unused-argument
353 try:
354 data = request.POST.dict()
356 if "description" in data:
357 description = data["description"]
358 user = request.user
359 assert isinstance(user, User)
360 for e in list(qs.filter(manually_settled=False)):
361 e.manually_settled = True
362 e.save(update_fields=["manually_settled"])
363 msg = "{}: {}".format(capfirst(_("marked as manually settled")), description)
364 admin_log([e], msg, who=user)
365 messages.info(request, "{}: {}".format(e, msg))
366 else:
367 cx = {
368 "qs": qs,
369 }
370 return render(request, "admin/jbank/mark_as_manually_settled.html", context=cx)
371 except ValidationError as e:
372 messages.error(request, " ".join(e.messages))
373 except Exception as e:
374 logger.error("mark_as_manually_settled: %s", traceback.format_exc())
375 messages.error(request, "{}".format(e))
376 return None
379def unmark_manually_settled_flag(modeladmin, request, qs): # pylint: disable=unused-argument
380 user = request.user
381 for e in list(qs.filter(manually_settled=True)):
382 e.manually_settled = False
383 e.save(update_fields=["manually_settled"])
384 msg = capfirst(_("manually settled flag cleared"))
385 admin_log([e], msg, who=user)
386 messages.info(request, "{}: {}".format(e, msg))
389def summarize_records(modeladmin, request, qs): # pylint: disable=unused-argument
390 total = Decimal("0.00")
391 out = "<table><tr><td style='text-align: left'>" + _("type") + "</td>" + "<td style='text-align: right'>" + _("amount") + "</td></tr>"
392 for e_type in qs.order_by("type").distinct("type"):
393 amt = sum_queryset(qs.filter(type=e_type.type))
394 type_name = e_type.type.name if e_type.type else ""
395 out += "<tr>"
396 out += "<td style='text-align: left'>" + type_name + "</td>"
397 out += "<td style='text-align: right'>" + localize(amt) + "</td>"
398 out += "</tr>"
399 total += amt
400 out += "<tr>"
401 out += "<td style='text-align: left'>" + _("total") + "</td>"
402 out += "<td style='text-align: right'>" + localize(total) + "</td>"
403 out += "</tr>"
404 out += "</table>"
405 messages.info(request, mark_safe(out))
408class StatementRecordAdmin(BankAdminBase):
409 list_per_page = 25
410 save_on_top = False
411 date_hierarchy = "value_date"
412 fields = (
413 "id",
414 "entry_type",
415 "statement",
416 "line_number",
417 "file_link",
418 "record_number",
419 "archive_identifier",
420 "record_date",
421 "value_date",
422 "paid_date",
423 "type",
424 "record_code",
425 "record_domain",
426 "family_code",
427 "sub_family_code",
428 "record_description",
429 "amount",
430 "receipt_code",
431 "delivery_method",
432 "name",
433 "name_source",
434 "recipient_account_number",
435 "recipient_account_number_changed",
436 "remittance_info",
437 "messages",
438 "client_messages",
439 "bank_messages",
440 "account",
441 "timestamp",
442 "created",
443 "last_modified",
444 "description",
445 "source_file",
446 "archived",
447 "manually_settled",
448 "is_settled_bool",
449 "child_links",
450 )
451 readonly_fields = fields
452 raw_id_fields = (
453 "statement",
454 # from AccountEntry
455 "account",
456 "source_file",
457 "parent",
458 "source_invoice",
459 "settled_invoice",
460 "settled_item",
461 )
462 list_filter = (
463 "statement__file__tag",
464 AccountNameFilter,
465 "manually_settled",
466 SettlementEntryTypesFilter,
467 "record_code",
468 )
469 search_fields = (
470 "=archive_identifier",
471 "=amount",
472 "=recipient_account_number",
473 "record_description",
474 "name",
475 "remittance_info",
476 "messages",
477 )
478 list_display = (
479 "id",
480 "value_date_short",
481 "type",
482 "record_code",
483 "amount",
484 "name",
485 "source_file_link",
486 "is_settled_bool",
487 )
488 inlines = (
489 StatementRecordSepaInfoInlineAdmin,
490 StatementRecordDetailInlineAdmin,
491 AccountEntryNoteInline,
492 )
493 actions = (
494 mark_as_manually_settled,
495 unmark_manually_settled_flag,
496 summarize_records,
497 )
499 def is_settled_bool(self, obj):
500 return obj.is_settled
502 is_settled_bool.short_description = _("settled") # type: ignore
503 is_settled_bool.boolean = True # type: ignore
505 def value_date_short(self, obj):
506 return date_format(obj.value_date, "SHORT_DATE_FORMAT")
508 value_date_short.short_description = _("date.short") # type: ignore
509 value_date_short.admin_order_field = "value_date" # type: ignore
511 def record_date_short(self, obj):
512 return date_format(obj.record_date, "SHORT_DATE_FORMAT")
514 record_date_short.short_description = _("record date") # type: ignore
515 record_date_short.admin_order_field = "record_date" # type: ignore
517 def child_links(self, obj) -> str:
518 assert isinstance(obj, StatementRecord)
519 return self.format_admin_obj_link_list(obj.child_set, "admin:jacc_accountentry_change") # type: ignore
521 child_links.short_description = _("derived entries") # type: ignore
523 def get_urls(self):
524 return [
525 path(
526 "by-statement/<int:statement_id>/",
527 self.admin_site.admin_view(self.kw_changelist_view),
528 name="jbank_statementrecord_statement_changelist",
529 ),
530 path(
531 "by-statement-file/<int:statement_file_id>/",
532 self.admin_site.admin_view(self.kw_changelist_view),
533 name="jbank_statementrecord_statementfile_changelist",
534 ),
535 ] + super().get_urls()
537 def get_queryset(self, request: HttpRequest):
538 rm = request.resolver_match
539 assert isinstance(rm, ResolverMatch)
540 qs = super().get_queryset(request)
541 statement_id = rm.kwargs.get("statement_id", None)
542 if statement_id:
543 qs = qs.filter(statement__id=statement_id)
544 statement_file_id = rm.kwargs.get("statement_file_id", None)
545 if statement_file_id:
546 qs = qs.filter(statement__file_id=statement_file_id)
547 return qs
549 def source_file_link(self, obj):
550 assert isinstance(obj, StatementRecord)
551 if not obj.statement:
552 return ""
553 admin_url = reverse("admin:jbank_statementfile_change", args=(obj.statement.file.id,))
554 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.statement.name)
556 source_file_link.admin_order_field = "statement" # type: ignore
557 source_file_link.short_description = _("source file") # type: ignore
559 def file_link(self, obj):
560 assert isinstance(obj, StatementRecord)
561 if not obj.statement or not obj.statement.file:
562 return ""
563 name = basename(obj.statement.file.file.name)
564 admin_url = reverse("admin:jbank_statementfile_change", args=(obj.statement.file.id,))
565 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), name)
567 file_link.admin_order_field = "file" # type: ignore
568 file_link.short_description = _("account statement file") # type: ignore
571class ReferencePaymentRecordAdmin(BankAdminBase):
572 exclude = ()
573 list_per_page = 25
574 save_on_top = False
575 date_hierarchy = "record_date"
576 raw_id_fields = (
577 "batch",
578 # from AccountEntry
579 "account",
580 "source_file",
581 "parent",
582 "source_invoice",
583 "settled_invoice",
584 "settled_item",
585 )
586 fields = [
587 "id",
588 "batch",
589 "line_number",
590 "file_link",
591 "record_type",
592 "account_number",
593 "record_date",
594 "paid_date",
595 "value_date",
596 "archive_identifier",
597 "remittance_info",
598 "payer_name",
599 "currency_identifier",
600 "name_source",
601 "amount",
602 "correction_identifier",
603 "delivery_method",
604 "receipt_code",
605 "archived",
606 "account",
607 "created",
608 "last_modified",
609 "timestamp",
610 "type",
611 "description",
612 "manually_settled",
613 "is_settled_bool",
614 "child_links",
615 "instructed_amount",
616 "instructed_currency",
617 "creditor_bank_bic",
618 "end_to_end_identifier",
619 ]
620 readonly_fields = (
621 "id",
622 "batch",
623 "line_number",
624 "file_link",
625 "record_type",
626 "account_number",
627 "record_date",
628 "paid_date",
629 "value_date",
630 "archive_identifier",
631 "remittance_info",
632 "payer_name",
633 "currency_identifier",
634 "name_source",
635 "amount",
636 "correction_identifier",
637 "delivery_method",
638 "receipt_code",
639 "archived",
640 "manually_settled",
641 "account",
642 "timestamp",
643 "created",
644 "last_modified",
645 "type",
646 "description",
647 "amount",
648 "source_file",
649 "source_invoice",
650 "settled_invoice",
651 "settled_item",
652 "parent",
653 "is_settled_bool",
654 "child_links",
655 "instructed_amount",
656 "instructed_currency",
657 "creditor_bank_bic",
658 "end_to_end_identifier",
659 )
660 list_filter = (
661 "batch__file__tag",
662 AccountNameFilter,
663 AccountEntryMatchedFilter,
664 "correction_identifier",
665 )
666 search_fields = (
667 "=archive_identifier",
668 "=amount",
669 "remittance_info",
670 "payer_name",
671 "batch__name",
672 )
673 list_display = (
674 "id",
675 "record_date",
676 "type",
677 "amount",
678 "payer_name",
679 "remittance_info",
680 "source_file_link",
681 "is_settled_bool",
682 )
683 actions = (
684 mark_as_manually_settled,
685 unmark_manually_settled_flag,
686 summarize_records,
687 )
688 inlines = [
689 AccountEntryNoteInline,
690 ]
692 def is_settled_bool(self, obj):
693 return obj.is_settled
695 is_settled_bool.short_description = _("settled") # type: ignore
696 is_settled_bool.boolean = True # type: ignore
698 def record_date_short(self, obj):
699 return date_format(obj.record_date, "SHORT_DATE_FORMAT")
701 record_date_short.short_description = _("date.short") # type: ignore
702 record_date_short.admin_order_field = "record_date" # type: ignore
704 def child_links(self, obj) -> str:
705 assert isinstance(obj, ReferencePaymentRecord)
706 return self.format_admin_obj_link_list(obj.child_set, "admin:jacc_accountentry_change") # type: ignore
708 child_links.short_description = _("derived entries") # type: ignore
710 def file_link(self, obj):
711 assert isinstance(obj, ReferencePaymentRecord)
712 if not obj.batch or not obj.batch.file:
713 return ""
714 admin_url = reverse("admin:jbank_referencepaymentbatchfile_change", args=(obj.batch.file.id,))
715 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.batch.file)
717 file_link.admin_order_field = "file" # type: ignore
718 file_link.short_description = _("file") # type: ignore
720 def get_urls(self):
721 return [
722 path(
723 "by-batch/<int:batch_id>/",
724 self.admin_site.admin_view(self.kw_changelist_view),
725 name="jbank_referencepaymentrecord_batch_changelist",
726 ),
727 path(
728 "by-statement-file/<int:stm_id>/",
729 self.admin_site.admin_view(self.kw_changelist_view),
730 name="jbank_referencepaymentrecord_statementfile_changelist",
731 ),
732 ] + super().get_urls()
734 def get_queryset(self, request: HttpRequest):
735 rm = request.resolver_match
736 assert isinstance(rm, ResolverMatch)
737 qs = super().get_queryset(request)
738 batch_id = rm.kwargs.get("batch_id", None)
739 if batch_id:
740 qs = qs.filter(batch_id=batch_id)
741 stm_id = rm.kwargs.get("stm_id", None)
742 if stm_id:
743 qs = qs.filter(batch__file_id=stm_id)
744 return qs
746 def source_file_link(self, obj):
747 assert isinstance(obj, ReferencePaymentRecord)
748 if not obj.batch:
749 return ""
750 admin_url = reverse("admin:jbank_referencepaymentbatchfile_change", args=(obj.batch.file.id,))
751 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.batch.name)
753 source_file_link.admin_order_field = "batch" # type: ignore
754 source_file_link.short_description = _("source file") # type: ignore
757class ReferencePaymentBatchAdmin(BankAdminBase):
758 exclude = ()
759 list_per_page = 20
760 save_on_top = False
761 ordering = ("-record_date",)
762 date_hierarchy = "record_date"
763 list_filter = ("record_set__account_number",)
764 fields = (
765 "file_link",
766 "record_date",
767 "institution_identifier",
768 "service_identifier",
769 "currency_identifier",
770 )
771 readonly_fields = (
772 "name",
773 "file",
774 "file_link",
775 "record_date",
776 "institution_identifier",
777 "service_identifier",
778 "currency_identifier",
779 )
780 search_fields = (
781 "name",
782 "=record_set__archive_identifier",
783 "=record_set__amount",
784 "record_set__remittance_info",
785 "record_set__payer_name",
786 )
787 list_display = (
788 "id",
789 "record_date_short",
790 "name",
791 "service_identifier",
792 "currency_identifier",
793 "account_entry_list",
794 )
796 def record_date_short(self, obj):
797 return date_format(obj.record_date, "SHORT_DATE_FORMAT")
799 record_date_short.short_description = _("record date") # type: ignore
800 record_date_short.admin_order_field = "record_date" # type: ignore
802 def account_entry_list(self, obj):
803 assert isinstance(obj, ReferencePaymentBatch)
804 admin_url = reverse("admin:jbank_referencepaymentrecord_batch_changelist", args=(obj.id,))
805 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), ReferencePaymentRecord.objects.filter(batch=obj).count())
807 account_entry_list.short_description = _("account entries") # type: ignore
809 def file_link(self, obj):
810 assert isinstance(obj, ReferencePaymentBatch)
811 if not obj.file:
812 return ""
813 admin_url = reverse("admin:jbank_referencepaymentbatchfile_change", args=(obj.file.id,))
814 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.file)
816 file_link.admin_order_field = "file" # type: ignore
817 file_link.short_description = _("file") # type: ignore
819 def get_urls(self):
820 return [
821 path(
822 "by-file/<int:file_id>/",
823 self.admin_site.admin_view(self.kw_changelist_view),
824 name="jbank_referencepaymentbatch_file_changelist",
825 ),
826 ] + super().get_urls()
828 def get_queryset(self, request: HttpRequest):
829 rm = request.resolver_match
830 assert isinstance(rm, ResolverMatch)
831 qs = super().get_queryset(request)
832 file_id = rm.kwargs.get("file_id", None)
833 if file_id:
834 qs = qs.filter(file=file_id)
835 return qs
838class StatementFileForm(forms.ModelForm):
839 class Meta:
840 fields = [
841 "file",
842 ]
844 def clean_file(self):
845 file = self.cleaned_data["file"]
846 assert isinstance(file, InMemoryUploadedFile)
847 name = file.name
848 file.seek(0)
849 content = file.read()
850 assert isinstance(content, bytes)
851 try:
852 statements = parse_tiliote_statements(content.decode("ISO-8859-1"), filename=basename(name))
853 for stm in statements:
854 account_number = stm["header"]["account_number"]
855 if Account.objects.filter(name=account_number).count() == 0:
856 raise ValidationError(_("account.not.found").format(account_number=account_number))
857 except ValidationError:
858 raise
859 except Exception as err:
860 raise ValidationError(_("Unhandled error") + ": {}".format(err)) from err
861 return file
864class StatementFileAdmin(BankAdminBase):
865 save_on_top = False
866 exclude = ()
867 form = StatementFileForm
869 date_hierarchy = "created"
871 search_fields = ("original_filename__contains",)
873 list_filter = ("tag",)
875 list_display = (
876 "id",
877 "created",
878 "file",
879 "records",
880 )
882 readonly_fields = (
883 "created",
884 "errors",
885 "file",
886 "original_filename",
887 "records",
888 )
890 def records(self, obj):
891 assert isinstance(obj, StatementFile)
892 admin_url = reverse("admin:jbank_statementrecord_statementfile_changelist", args=[obj.id])
893 return format_html('<a href="{}">{}</a>', admin_url, _("statement records"))
895 records.short_description = _("statement records") # type: ignore
897 def has_add_permission(self, request: HttpRequest) -> bool:
898 return False
900 def construct_change_message(self, request, form, formsets, add=False):
901 if add:
902 instance = form.instance
903 assert isinstance(instance, StatementFile)
904 if instance.file:
905 full_path = instance.full_path
906 plain_filename = basename(full_path)
907 try:
908 statements = parse_tiliote_statements_from_file(full_path)
909 with transaction.atomic():
910 for data in statements:
911 create_statement(data, name=plain_filename, file=instance)
912 except Exception as e:
913 instance.errors = traceback.format_exc()
914 instance.save()
915 add_message(request, ERROR, str(e))
916 instance.delete()
918 return super().construct_change_message(request, form, formsets, add)
921class ReferencePaymentBatchFileForm(forms.ModelForm):
922 class Meta:
923 fields = [
924 "file",
925 ]
927 def clean_file(self):
928 file = self.cleaned_data["file"]
929 assert isinstance(file, InMemoryUploadedFile)
930 name = file.name
931 file.seek(0)
932 content = file.read()
933 assert isinstance(content, bytes)
934 try:
935 batches = parse_svm_batches(content.decode("ISO-8859-1"), filename=basename(name))
936 for b in batches:
937 for rec in b["records"]:
938 account_number = rec["account_number"]
939 if Account.objects.filter(name=account_number).count() == 0:
940 raise ValidationError(_("account.not.found").format(account_number=account_number))
941 except ValidationError:
942 raise
943 except Exception as err:
944 raise ValidationError(_("Unhandled error") + ": {}".format(err)) from err
945 return file
948class ReferencePaymentBatchFileAdmin(BankAdminBase):
949 save_on_top = False
950 exclude = ()
951 form = ReferencePaymentBatchFileForm
952 date_hierarchy = "created"
954 list_display = (
955 "id",
956 "created",
957 "file",
958 "total",
959 )
961 list_filter = ("tag",)
963 search_fields = ("file__contains",)
965 readonly_fields = (
966 "created",
967 "errors",
968 "file",
969 "original_filename",
970 )
972 def has_add_permission(self, request: HttpRequest) -> bool:
973 return False
975 def total(self, obj):
976 assert isinstance(obj, ReferencePaymentBatchFile)
977 path = reverse("admin:jbank_referencepaymentrecord_statementfile_changelist", args=[obj.id])
978 return format_html('<a href="{}">{}</a>', path, localize(obj.total_amount))
980 total.short_description = _("total amount") # type: ignore
982 def construct_change_message(self, request, form, formsets, add=False):
983 if add:
984 instance = form.instance
985 assert isinstance(instance, ReferencePaymentBatchFile)
986 if instance.file:
987 full_path = instance.full_path
988 plain_filename = basename(full_path)
989 try:
990 batches = parse_svm_batches_from_file(full_path)
991 with transaction.atomic():
992 for data in batches:
993 create_reference_payment_batch(data, name=plain_filename, file=instance)
994 except Exception as e:
995 user = request.user
996 assert isinstance(user, User)
997 instance.errors = traceback.format_exc()
998 instance.save()
999 msg = str(e)
1000 if user.is_superuser:
1001 msg = instance.errors
1002 logger.error("%s: %s", plain_filename, msg)
1003 add_message(request, ERROR, msg)
1004 instance.delete()
1006 return super().construct_change_message(request, form, formsets, add)
1009class PayoutStatusAdminMixin:
1010 def created_brief(self, obj):
1011 assert isinstance(obj, PayoutStatus)
1012 return date_format(obj.created, "SHORT_DATETIME_FORMAT")
1014 created_brief.short_description = _("created") # type: ignore
1015 created_brief.admin_order_field = "created" # type: ignore
1017 def timestamp_brief(self, obj):
1018 assert isinstance(obj, PayoutStatus)
1019 return date_format(obj.timestamp, "SHORT_DATETIME_FORMAT") if obj.timestamp else ""
1021 timestamp_brief.short_description = _("timestamp") # type: ignore
1022 timestamp_brief.admin_order_field = "timestamp" # type: ignore
1024 def file_name_link(self, obj):
1025 assert isinstance(obj, PayoutStatus)
1026 if obj.id is None or not obj.full_path:
1027 return obj.file_name
1028 admin_url = reverse(
1029 "admin:jbank_payoutstatus_file_download",
1030 args=(
1031 obj.id,
1032 obj.file_name,
1033 ),
1034 )
1035 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.file_name)
1037 file_name_link.short_description = _("file") # type: ignore
1038 file_name_link.admin_order_field = "file_name" # type: ignore
1041class PayoutStatusAdmin(BankAdminBase, PayoutStatusAdminMixin):
1042 fields = (
1043 "created_brief",
1044 "timestamp_brief",
1045 "payout",
1046 "file_name_link",
1047 "response_code",
1048 "response_text",
1049 "msg_id",
1050 "original_msg_id",
1051 "group_status",
1052 "status_reason",
1053 )
1054 date_hierarchy = "created"
1055 readonly_fields = fields
1056 list_filter = [
1057 "group_status",
1058 ]
1059 list_display = (
1060 "id",
1061 "created_brief",
1062 "timestamp_brief",
1063 "payout",
1064 "file_name_link",
1065 "response_code",
1066 "response_text",
1067 "original_msg_id",
1068 "group_status",
1069 )
1071 def file_download_view(self, request, pk, filename, form_url="", extra_context=None): # pylint: disable=unused-argument
1072 user = request.user
1073 if not user.is_authenticated or not user.is_staff:
1074 raise Http404(_("File {} not found").format(filename))
1075 obj = get_object_or_404(self.get_queryset(request), pk=pk, file_name=filename)
1076 assert isinstance(obj, PayoutStatus)
1077 full_path = obj.full_path
1078 if not os.path.isfile(full_path):
1079 raise Http404(_("File {} not found").format(filename))
1080 return FormattedXmlFileResponse(full_path)
1082 def get_urls(self):
1083 urls = [
1084 re_path(
1085 r"^(\d+)/change/status-downloads/(.+)/$",
1086 self.file_download_view,
1087 name="jbank_payoutstatus_file_download",
1088 ),
1089 ]
1090 return urls + super().get_urls()
1093class PayoutStatusInlineAdmin(admin.TabularInline, PayoutStatusAdminMixin):
1094 model = PayoutStatus
1095 can_delete = False
1096 extra = 0
1097 ordering = ("-id",)
1098 fields = (
1099 "created_brief",
1100 "timestamp_brief",
1101 "payout",
1102 "file_name_link",
1103 "response_code",
1104 "response_text",
1105 "msg_id",
1106 "original_msg_id",
1107 "group_status",
1108 "status_reason",
1109 )
1110 readonly_fields = fields
1113def send_payouts_to_bank(modeladmin, request, qs): # pylint: disable=unused-argument
1114 user_ip = get_ip(request)
1115 for p in list(qs.order_by("id").distinct()):
1116 assert isinstance(p, Payout)
1117 try:
1118 p.refresh_from_db()
1119 if p.state not in [PAYOUT_ERROR, PAYOUT_ON_HOLD]:
1120 messages.warning(request, f"{p}: {p.state_name}")
1121 continue
1122 old_state = p.state_name
1123 p.state = PAYOUT_WAITING_PROCESSING
1124 p.save(update_fields=["state"])
1125 admin_log([p], f"Changed state from {old_state} to {p.state_name}", who=request.user, ip=user_ip)
1126 messages.success(request, f"{p}: {p.state_name}")
1127 except Exception as err:
1128 messages.error(request, f"{p}: {err}")
1131def mark_payouts_as_paid(modeladmin, request, qs): # pylint: disable=unused-argument
1132 user_ip = get_ip(request)
1133 for p in list(qs.order_by("id").distinct()):
1134 assert isinstance(p, Payout)
1135 try:
1136 if p.state == PAYOUT_PAID:
1137 messages.warning(request, f"{p}: {p.state_name}")
1138 continue
1139 p.state = PAYOUT_PAID
1140 p.save(update_fields=["state"])
1141 admin_log([p], f"Changed state to {p.state_name}", who=request.user, ip=user_ip)
1142 messages.success(request, f"{p}: {p.state_name}")
1143 except Exception as err:
1144 messages.error(request, f"{p}: {err}")
1147def regenerate_payout_message_identifiers(modeladmin, request, qs): # pylint: disable=unused-argument
1148 user_ip = get_ip(request)
1149 n_count = 0
1150 for p in list(qs.order_by("id").distinct()):
1151 assert isinstance(p, Payout)
1152 try:
1153 if p.state == PAYOUT_PAID:
1154 messages.warning(request, f"{p}: {p.state_name}")
1155 continue
1156 old = p.msg_id
1157 p.generate_msg_id(commit=False)
1158 p.file_name = ""
1159 p.save(update_fields=["msg_id", "file_name"])
1160 admin_log([p], f"Regenerated msg_id from {old} to {p.msg_id}, file name reset", who=request.user, ip=user_ip)
1161 n_count += 1
1162 except Exception as err:
1163 messages.error(request, f"{p}: {err}")
1164 messages.success(request, f"{n_count} message IDs regenerated and pain001 file names reset")
1167class PayoutAdmin(BankAdminBase):
1168 save_on_top = False
1169 inlines = [PayoutStatusInlineAdmin, AccountEntryNoteInline]
1170 date_hierarchy = "timestamp"
1172 actions = [
1173 send_payouts_to_bank,
1174 mark_payouts_as_paid,
1175 regenerate_payout_message_identifiers,
1176 ]
1178 raw_id_fields: Sequence[str] = (
1179 "account",
1180 "parent",
1181 "payer",
1182 "recipient",
1183 )
1185 list_filter = [
1186 "state",
1187 "payoutstatus_set__response_code",
1188 "payoutstatus_set__group_status",
1189 "recipient__bic",
1190 ]
1192 fields = [
1193 "connection",
1194 "account",
1195 "parent",
1196 "payer",
1197 "recipient",
1198 "amount",
1199 "messages",
1200 "reference",
1201 "due_date",
1202 "msg_id",
1203 "file_name",
1204 "timestamp",
1205 "paid_date",
1206 "state",
1207 "group_status",
1208 "created",
1209 ]
1211 list_display = [
1212 "id",
1213 "timestamp",
1214 "recipient",
1215 "amount",
1216 "paid_date_brief",
1217 "state",
1218 ]
1220 readonly_fields = [
1221 "created",
1222 "paid_date",
1223 "timestamp",
1224 "msg_id",
1225 "file_name",
1226 "group_status",
1227 ]
1229 search_fields = [
1230 "=msg_id",
1231 "=file_name",
1232 "=file_reference",
1233 "recipient__name",
1234 "=recipient__account_number",
1235 "=msg_id",
1236 "=amount",
1237 ]
1239 def paid_date_brief(self, obj):
1240 assert isinstance(obj, Payout)
1241 return date_format(obj.paid_date, "SHORT_DATETIME_FORMAT") if obj.paid_date else ""
1243 paid_date_brief.short_description = _("paid date") # type: ignore
1244 paid_date_brief.admin_order_field = "paid_date" # type: ignore
1246 def save_model(self, request, obj, form, change):
1247 assert isinstance(obj, Payout)
1248 if not change:
1249 if not hasattr(obj, "account") or not obj.account:
1250 obj.account = obj.payer.payouts_account
1251 if not hasattr(obj, "type") or not obj.type:
1252 obj.type = EntryType.objects.get(code=settings.E_BANK_PAYOUT)
1253 return super().save_model(request, obj, form, change)
1256class PayoutPartyAdmin(BankAdminBase):
1257 save_on_top = False
1258 search_fields = (
1259 "name",
1260 "=account_number",
1261 "=org_id",
1262 )
1263 ordering = ("name",)
1264 fields = [
1265 "name",
1266 "account_number",
1267 "bic",
1268 "org_id",
1269 "address",
1270 "country_code",
1271 "payouts_account",
1272 ]
1273 readonly_fields: List[str] = []
1274 actions = ()
1276 list_display = (
1277 "id",
1278 "name",
1279 "account_number",
1280 "bic",
1281 "org_id",
1282 "address",
1283 "country_code",
1284 )
1286 raw_id_fields = ("payouts_account",)
1288 def get_readonly_fields(self, request: HttpRequest, obj=None) -> Sequence[str]:
1289 if obj is not None:
1290 assert isinstance(obj, PayoutParty)
1291 if Payout.objects.filter(Q(recipient=obj) | Q(payer=obj)).exists():
1292 return self.fields
1293 return self.readonly_fields
1296class RefundAdmin(PayoutAdmin):
1297 raw_id_fields = [
1298 "account",
1299 "parent",
1300 "payer",
1301 "recipient",
1302 ]
1303 fields = [
1304 "connection",
1305 "account",
1306 "payer",
1307 "parent",
1308 "recipient",
1309 "amount",
1310 "messages",
1311 "reference",
1312 "attachment",
1313 "msg_id",
1314 "file_name",
1315 "timestamp",
1316 "paid_date",
1317 "group_status",
1318 "created",
1319 ]
1320 readonly_fields = [
1321 "msg_id",
1322 "file_name",
1323 "timestamp",
1324 "paid_date",
1325 "group_status",
1326 "created",
1327 ]
1328 inlines = [AccountEntryNoteInline]
1331class CurrencyExchangeSourceAdmin(BankAdminBase):
1332 save_on_top = False
1333 exclude = ()
1335 fields = (
1336 "id",
1337 "created",
1338 "name",
1339 )
1341 readonly_fields = (
1342 "id",
1343 "created",
1344 )
1346 list_display = fields
1349class CurrencyExchangeAdmin(BankAdminBase):
1350 save_on_top = False
1352 fields = (
1353 "record_date",
1354 "source_currency",
1355 "target_currency",
1356 "unit_currency",
1357 "exchange_rate",
1358 "source",
1359 )
1361 date_hierarchy = "record_date"
1362 readonly_fields = list_display = fields
1363 raw_id_fields = ("source",)
1364 list_filter = ("source_currency", "target_currency", "source")
1367class WsEdiConnectionAdmin(BankAdminBase):
1368 save_on_top = False
1370 ordering = [
1371 "name",
1372 ]
1374 list_display = (
1375 "id",
1376 "created",
1377 "name",
1378 "sender_identifier",
1379 "receiver_identifier",
1380 "expires",
1381 )
1383 raw_id_fields = ()
1385 fieldsets = (
1386 (
1387 None,
1388 {
1389 "fields": [
1390 "id",
1391 "name",
1392 "enabled",
1393 "sender_identifier",
1394 "receiver_identifier",
1395 "target_identifier",
1396 "environment",
1397 "debug_commands",
1398 "created",
1399 ]
1400 },
1401 ),
1402 (
1403 "PKI",
1404 {
1405 "fields": [
1406 "pki_endpoint",
1407 "pin",
1408 "bank_root_cert_file",
1409 ]
1410 },
1411 ),
1412 (
1413 "EDI",
1414 {
1415 "fields": [
1416 "soap_endpoint",
1417 "signing_cert_file",
1418 "signing_key_file",
1419 "encryption_cert_file",
1420 "encryption_key_file",
1421 "bank_encryption_cert_file",
1422 "bank_signing_cert_file",
1423 "ca_cert_file",
1424 ]
1425 },
1426 ),
1427 )
1429 readonly_fields = (
1430 "id",
1431 "created",
1432 "expires",
1433 )
1435 def expires(self, obj):
1436 assert isinstance(obj, WsEdiConnection)
1437 min_not_valid_after: Optional[datetime] = None
1438 try:
1439 certs = [
1440 obj.signing_cert_full_path,
1441 obj.encryption_cert_full_path,
1442 obj.bank_encryption_cert_full_path,
1443 obj.bank_root_cert_full_path,
1444 obj.ca_cert_full_path,
1445 ]
1446 except Exception as e:
1447 logger.error(e)
1448 return _("(missing certificate files)")
1449 for filename in certs:
1450 if filename and os.path.isfile(filename):
1451 cert = get_x509_cert_from_file(filename)
1452 not_valid_after = pytz.utc.localize(cert.not_valid_after)
1453 if min_not_valid_after is None or not_valid_after < min_not_valid_after:
1454 min_not_valid_after = not_valid_after
1455 return date_format(min_not_valid_after.date(), "SHORT_DATE_FORMAT") if min_not_valid_after else ""
1457 expires.short_description = _("expires") # type: ignore
1460class WsEdiSoapCallAdmin(BankAdminBase):
1461 save_on_top = False
1463 date_hierarchy = "created"
1465 list_display = (
1466 "id",
1467 "created",
1468 "connection",
1469 "command",
1470 "executed",
1471 "execution_time",
1472 )
1474 list_filter = (
1475 "connection",
1476 "command",
1477 )
1479 raw_id_fields = ()
1481 fields = (
1482 "id",
1483 "connection",
1484 "command",
1485 "created",
1486 "executed",
1487 "execution_time",
1488 "error_fmt",
1489 "admin_application_request",
1490 "admin_application_response",
1491 "admin_application_response_file",
1492 )
1494 readonly_fields = (
1495 "id",
1496 "connection",
1497 "command",
1498 "created",
1499 "executed",
1500 "execution_time",
1501 "error_fmt",
1502 "admin_application_request",
1503 "admin_application_response",
1504 "admin_application_response_file",
1505 )
1507 def get_fields(self, request, obj=None):
1508 fields = super().get_fields(request, obj)
1509 if not request.user.is_superuser:
1510 fields = fields[:-2]
1511 return fields
1513 def soap_download_view(self, request, object_id, file_type: str, form_url="", extra_context=None): # pylint: disable=unused-argument
1514 user = request.user
1515 if not user.is_authenticated or not user.is_superuser:
1516 raise Http404("File not found")
1517 obj = get_object_or_404(self.get_queryset(request), id=object_id)
1518 assert isinstance(obj, WsEdiSoapCall)
1519 if file_type == "f":
1520 with open(obj.debug_response_full_path, "rb") as fb:
1521 data = xml_to_dict(fb.read())
1522 content = base64.b64decode(data.get("Content", ""))
1523 return FormattedXmlResponse(content, filename=obj.debug_get_filename(file_type))
1524 return FormattedXmlFileResponse(WsEdiSoapCall.debug_get_file_path(obj.debug_get_filename(file_type)))
1526 def get_urls(self) -> List[URLPattern]:
1527 info = self.model._meta.app_label, self.model._meta.model_name # noqa
1528 return [
1529 path("<path:object_id>/soap-download/<path:file_type>/", self.admin_site.admin_view(self.soap_download_view), name="%s_%s_soap_download" % info),
1530 ] + super().get_urls()
1532 def admin_application_request(self, obj):
1533 assert isinstance(obj, WsEdiSoapCall)
1534 if not os.path.isfile(obj.debug_request_full_path):
1535 return ""
1536 download_url = reverse("admin:jbank_wsedisoapcall_soap_download", args=[str(obj.id), "q"])
1537 return mark_safe(format_html('<a href="{}">{}</a>', download_url, os.path.basename(obj.debug_request_full_path)))
1539 admin_application_request.short_description = _("application request") # type: ignore
1541 def admin_application_response(self, obj):
1542 assert isinstance(obj, WsEdiSoapCall)
1543 if not os.path.isfile(obj.debug_response_full_path):
1544 return ""
1545 download_url = reverse("admin:jbank_wsedisoapcall_soap_download", args=[str(obj.id), "s"])
1546 return mark_safe(format_html('<a href="{}">{}</a>', download_url, os.path.basename(obj.debug_response_full_path)))
1548 admin_application_response.short_description = _("application response") # type: ignore
1550 def admin_application_response_file(self, obj):
1551 assert isinstance(obj, WsEdiSoapCall)
1552 if obj.command != "DownloadFile" or not obj.executed:
1553 return ""
1554 file_type = "f"
1555 download_url = reverse("admin:jbank_wsedisoapcall_soap_download", args=[str(obj.id), file_type])
1556 return mark_safe(format_html('<a href="{}">{}</a>', download_url, obj.debug_get_filename(file_type)))
1558 admin_application_response_file.short_description = _("file") # type: ignore
1560 def execution_time(self, obj):
1561 assert isinstance(obj, WsEdiSoapCall)
1562 return obj.executed - obj.created if obj.executed else ""
1564 execution_time.short_description = _("execution time") # type: ignore
1566 def error_fmt(self, obj):
1567 assert isinstance(obj, WsEdiSoapCall)
1568 return mark_safe(obj.error.replace("\n", "<br>"))
1570 error_fmt.short_description = _("error") # type: ignore
1573class EuriborRateAdmin(BankAdminBase):
1574 save_on_top = False
1575 fields = [
1576 "record_date",
1577 "name",
1578 "rate",
1579 "created",
1580 ]
1581 date_hierarchy = "record_date"
1582 list_filter = [
1583 "name",
1584 ]
1585 list_display = [
1586 "id",
1587 "record_date",
1588 "name",
1589 "rate",
1590 ]
1591 readonly_fields = [
1592 "id",
1593 "created",
1594 ]
1597class AccountBalanceAdmin(BankAdminBase):
1598 save_on_top = False
1599 fields = [
1600 "id",
1601 "record_datetime_fmt",
1602 "account_number",
1603 "bic",
1604 "balance_fmt",
1605 "available_balance_fmt",
1606 "credit_limit_fmt",
1607 "currency",
1608 "created_fmt",
1609 ]
1610 date_hierarchy = "record_datetime"
1611 list_filter = [
1612 "account_number",
1613 "bic",
1614 "currency",
1615 ]
1616 list_display = [
1617 "id",
1618 "record_datetime_fmt",
1619 "account_number",
1620 "bic",
1621 "balance_fmt",
1622 "available_balance_fmt",
1623 "credit_limit_fmt",
1624 "currency",
1625 "created_fmt",
1626 ]
1627 readonly_fields = fields
1629 def has_add_permission(self, request) -> bool: # type: ignore # noqa
1630 return False
1632 @admin.display(description=_("balance"), ordering="balance")
1633 def balance_fmt(self, obj):
1634 assert isinstance(obj, AccountBalance)
1635 return self.format_currency(obj.balance)
1637 @admin.display(description=_("available balance"), ordering="available_balance")
1638 def available_balance_fmt(self, obj):
1639 assert isinstance(obj, AccountBalance)
1640 return self.format_currency(obj.available_balance)
1642 @admin.display(description=_("credit limit"), ordering="available_balance")
1643 def credit_limit_fmt(self, obj):
1644 assert isinstance(obj, AccountBalance)
1645 return self.format_currency(obj.credit_limit)
1647 @admin.display(description=_("created"), ordering="created")
1648 def created_fmt(self, obj):
1649 assert isinstance(obj, AccountBalance)
1650 return format_timedelta(now() - obj.created)
1652 @admin.display(description=_("record date"), ordering="record_datetime")
1653 def record_datetime_fmt(self, obj):
1654 assert isinstance(obj, AccountBalance)
1655 return date_format(obj.record_datetime, "SHORT_DATETIME_FORMAT")
1658mark_as_manually_settled.short_description = _("Mark as manually settled") # type: ignore
1659unmark_manually_settled_flag.short_description = _("Unmark manually settled flag") # type: ignore
1660send_payouts_to_bank.short_description = _("Send payouts to bank") # type: ignore
1661mark_payouts_as_paid.short_description = _("Mark payouts as paid") # type: ignore
1662regenerate_payout_message_identifiers.short_description = _("Regenerate payout message identifiers") # type: ignore
1664admin.site.register(CurrencyExchangeSource, CurrencyExchangeSourceAdmin)
1665admin.site.register(CurrencyExchange, CurrencyExchangeAdmin)
1666admin.site.register(Payout, PayoutAdmin)
1667admin.site.register(PayoutStatus, PayoutStatusAdmin)
1668admin.site.register(PayoutParty, PayoutPartyAdmin)
1669admin.site.register(Refund, RefundAdmin)
1670admin.site.register(Statement, StatementAdmin)
1671admin.site.register(StatementRecord, StatementRecordAdmin)
1672admin.site.register(StatementFile, StatementFileAdmin)
1673admin.site.register(ReferencePaymentRecord, ReferencePaymentRecordAdmin)
1674admin.site.register(ReferencePaymentBatch, ReferencePaymentBatchAdmin)
1675admin.site.register(ReferencePaymentBatchFile, ReferencePaymentBatchFileAdmin)
1676admin.site.register(WsEdiConnection, WsEdiConnectionAdmin)
1677admin.site.register(WsEdiSoapCall, WsEdiSoapCallAdmin)
1678admin.site.register(EuriborRate, EuriborRateAdmin)
1679admin.site.register(AccountBalance, AccountBalanceAdmin)