Coverage for jbank/admin.py: 48%

1# pylint: disable=too-many-arguments

2 import base64

3 import logging

4 import os

5 import traceback

6 from datetime import datetime

7 from decimal import Decimal

8 from os.path import basename

9 from typing import Optional, Sequence, List

10 import pytz

11 from django import forms

12 from django.conf import settings

13 from django.contrib import admin

14 from django.contrib import messages

15 from django.contrib.admin import SimpleListFilter

16 from django.contrib.auth.models import User # noqa

17 from django.contrib.messages import add_message, ERROR

18 from django.core.exceptions import ValidationError

19 from django.core.files.uploadedfile import InMemoryUploadedFile

20 from django.db import transaction

21 from django.db.models import F, Q, QuerySet

22 from django.db.models.aggregates import Sum

23 from django.http import HttpRequest, Http404

24 from django.shortcuts import render, get_object_or_404

25 from django.urls import ResolverMatch, reverse, path, re_path, URLPattern

26 from django.utils.formats import date_format, localize

27 from django.utils.html import format_html

28 from django.utils.safestring import mark_safe

29 from django.utils.text import capfirst

30 from django.utils.timezone import now

31 from django.utils.translation import gettext_lazy as _

32 from jacc.admin import AccountEntryNoteInline, AccountEntryNoteAdmin

33 from jacc.helpers import sum_queryset

34 from jacc.models import Account, EntryType, AccountEntryNote

35 from jbank.x509_helpers import get_x509_cert_from_file

36 from jutil.format import dec2, format_timedelta

37 from jutil.request import get_ip

38 from jutil.responses import FormattedXmlResponse, FormattedXmlFileResponse

39 from jutil.validators import iban_bic

40 from jutil.xml import xml_to_dict

41 from 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)

66 from jbank.tito import parse_tiliote_statements_from_file, parse_tiliote_statements

67 from jbank.svm import parse_svm_batches_from_file, parse_svm_batches, create_statement, create_reference_payment_batch

68 from jutil.admin import ModelAdminBase, admin_log

69

70 logger = logging.getLogger(__name__)

71

72

73 class BankAdminBase(ModelAdminBase):

74 save_on_top = False

75

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()

81

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)

91

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 = "&nbsp;" + value_str[-3:] + out

101 value_str = value_str[:-3]

102 out = value_str + out

103 return mark_safe(out)

104

105

106 class SettlementEntryTypesFilter(SimpleListFilter):

107 """

108 Filters incoming settlement type entries.

109 """

110

111 title = _("account entry types")

112 parameter_name = "type"

113

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

120

121 def queryset(self, request, queryset):

122 val = self.value()

123 if val:

124 return queryset.filter(type__id=val)

125 return queryset

126

127

128 class AccountEntryMatchedFilter(SimpleListFilter):

129 """

130 Filters incoming payments which do not have any child/derived account entries.

131 """

132

133 title = _("account.entry.matched.filter")

134 parameter_name = "matched"

135

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 ]

143

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

165

166

167 class AccountNameFilter(SimpleListFilter):

168 """

169 Filters account entries based on account name.

170 """

171

172 title = _("account.name.filter")

173 parameter_name = "account-name"

174

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])

181

182 def queryset(self, request, queryset):

183 val = self.value()

184 if val:

185 return queryset.filter(account__name=val)

186 return queryset

187

188

189 class 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 )

234

235 def bic_code(self, obj):

236 assert isinstance(obj, Statement)

237 return iban_bic(obj.account_number)

238

239 bic_code.short_description = "BIC" # type: ignore

240

241 def statement_date_short(self, obj):

242 return date_format(obj.end_date, "SHORT_DATE_FORMAT")

243

244 statement_date_short.short_description = _("date") # type: ignore

245 statement_date_short.admin_order_field = "end_date" # type: ignore

246

247 def record_date_short(self, obj):

248 return date_format(obj.record_date, "SHORT_DATE_FORMAT")

249

250 record_date_short.short_description = _("record date") # type: ignore

251 record_date_short.admin_order_field = "record_date" # type: ignore

252

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())

257

258 account_entry_list.short_description = _("account entries") # type: ignore

259

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)

266

267 file_link.admin_order_field = "file" # type: ignore

268 file_link.short_description = _("file") # type: ignore

269

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()

278

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

287

288

289 class StatementRecordDetailInlineAdmin(admin.StackedInline):

290 exclude = ()

291 model = StatementRecordDetail

292 can_delete = False

293 extra = 0

294

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 = ()

314

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))

322

323 structured_remittance_info.short_description = _("structured remittance info") # type: ignore

324

325 def has_add_permission(self, request, obj=None):

326 return False

327

328

329 class StatementRecordSepaInfoInlineAdmin(admin.StackedInline):

330 exclude = ()

331 model = StatementRecordSepaInfo

332 can_delete = False

333 extra = 0

334 max_num = 1

335

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",)

347

348 def has_add_permission(self, request, obj=None):

349 return False

350

351

352 def mark_as_manually_settled(modeladmin, request, qs): # pylint: disable=unused-argument

353 try:

354 data = request.POST.dict()

355

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

377

378

379 def 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))

387

388

389 def 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))

406

407

408 class 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 )

498

499 def is_settled_bool(self, obj):

500 return obj.is_settled

501

502 is_settled_bool.short_description = _("settled") # type: ignore

503 is_settled_bool.boolean = True # type: ignore

504

505 def value_date_short(self, obj):

506 return date_format(obj.value_date, "SHORT_DATE_FORMAT")

507

508 value_date_short.short_description = _("date.short") # type: ignore

509 value_date_short.admin_order_field = "value_date" # type: ignore

510

511 def record_date_short(self, obj):

512 return date_format(obj.record_date, "SHORT_DATE_FORMAT")

513

514 record_date_short.short_description = _("record date") # type: ignore

515 record_date_short.admin_order_field = "record_date" # type: ignore

516

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

520

521 child_links.short_description = _("derived entries") # type: ignore

522

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()

536

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

548

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)

555

556 source_file_link.admin_order_field = "statement" # type: ignore

557 source_file_link.short_description = _("source file") # type: ignore

558

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)

566

567 file_link.admin_order_field = "file" # type: ignore

568 file_link.short_description = _("account statement file") # type: ignore

569

570

571 class 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 ]

691

692 def is_settled_bool(self, obj):

693 return obj.is_settled

694

695 is_settled_bool.short_description = _("settled") # type: ignore

696 is_settled_bool.boolean = True # type: ignore

697

698 def record_date_short(self, obj):

699 return date_format(obj.record_date, "SHORT_DATE_FORMAT")

700

701 record_date_short.short_description = _("date.short") # type: ignore

702 record_date_short.admin_order_field = "record_date" # type: ignore

703

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

707

708 child_links.short_description = _("derived entries") # type: ignore

709

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)

716

717 file_link.admin_order_field = "file" # type: ignore

718 file_link.short_description = _("file") # type: ignore

719

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()

733

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

745

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)

752

753 source_file_link.admin_order_field = "batch" # type: ignore

754 source_file_link.short_description = _("source file") # type: ignore

755

756

757 class 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 )

795

796 def record_date_short(self, obj):

797 return date_format(obj.record_date, "SHORT_DATE_FORMAT")

798

799 record_date_short.short_description = _("record date") # type: ignore

800 record_date_short.admin_order_field = "record_date" # type: ignore

801

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())

806

807 account_entry_list.short_description = _("account entries") # type: ignore

808

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)

815

816 file_link.admin_order_field = "file" # type: ignore

817 file_link.short_description = _("file") # type: ignore

818

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()

827

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

836

837

838 class StatementFileForm(forms.ModelForm):

839 class Meta:

840 fields = [

841 "file",

842 ]

843

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

862

863

864 class StatementFileAdmin(BankAdminBase):

865 save_on_top = False

866 exclude = ()

867 form = StatementFileForm

868

869 date_hierarchy = "created"

870

871 search_fields = ("original_filename__contains",)

872

873 list_filter = ("tag",)

874

875 list_display = (

876 "id",

877 "created",

878 "file",

879 "records",

880 )

881

882 readonly_fields = (

883 "created",

884 "errors",

885 "file",

886 "original_filename",

887 "records",

888 )

889

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"))

894

895 records.short_description = _("statement records") # type: ignore

896

897 def has_add_permission(self, request: HttpRequest) -> bool:

898 return False

899

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()

917

918 return super().construct_change_message(request, form, formsets, add)

919

920

921 class ReferencePaymentBatchFileForm(forms.ModelForm):

922 class Meta:

923 fields = [

924 "file",

925 ]

926

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

946

947

948 class ReferencePaymentBatchFileAdmin(BankAdminBase):

949 save_on_top = False

950 exclude = ()

951 form = ReferencePaymentBatchFileForm

952 date_hierarchy = "created"

953

954 list_display = (

955 "id",

956 "created",

957 "file",

958 "total",

959 )

960

961 list_filter = ("tag",)

962

963 search_fields = ("file__contains",)

964

965 readonly_fields = (

966 "created",

967 "errors",

968 "file",

969 "original_filename",

970 )

971

972 def has_add_permission(self, request: HttpRequest) -> bool:

973 return False

974

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))

979

980 total.short_description = _("total amount") # type: ignore

981

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()

1005

1006 return super().construct_change_message(request, form, formsets, add)

1007

1008

1009 class PayoutStatusAdminMixin:

1010 def created_brief(self, obj):

1011 assert isinstance(obj, PayoutStatus)

1012 return date_format(obj.created, "SHORT_DATETIME_FORMAT")

1013

1014 created_brief.short_description = _("created") # type: ignore

1015 created_brief.admin_order_field = "created" # type: ignore

1016

1017 def timestamp_brief(self, obj):

1018 assert isinstance(obj, PayoutStatus)

1019 return date_format(obj.timestamp, "SHORT_DATETIME_FORMAT") if obj.timestamp else ""

1020

1021 timestamp_brief.short_description = _("timestamp") # type: ignore

1022 timestamp_brief.admin_order_field = "timestamp" # type: ignore

1023

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)

1036

1037 file_name_link.short_description = _("file") # type: ignore

1038 file_name_link.admin_order_field = "file_name" # type: ignore

1039

1040

1041 class 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 )

1070

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)

1081

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()

1091

1092

1093 class 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

1111

1112

1113 def 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}")

1129

1130

1131 def 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}")

1145

1146

1147 def 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")

1165

1166

1167 class PayoutAdmin(BankAdminBase):

1168 save_on_top = False

1169 inlines = [PayoutStatusInlineAdmin, AccountEntryNoteInline]

1170 date_hierarchy = "timestamp"

1171

1172 actions = [

1173 send_payouts_to_bank,

1174 mark_payouts_as_paid,

1175 regenerate_payout_message_identifiers,

1176 ]

1177

1178 raw_id_fields: Sequence[str] = (

1179 "account",

1180 "parent",

1181 "payer",

1182 "recipient",

1183 )

1184

1185 list_filter = [

1186 "state",

1187 "payoutstatus_set__response_code",

1188 "payoutstatus_set__group_status",

1189 "recipient__bic",

1190 ]

1191

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 ]

1210

1211 list_display = [

1212 "id",

1213 "timestamp",

1214 "recipient",

1215 "amount",

1216 "paid_date_brief",

1217 "state",

1218 ]

1219

1220 readonly_fields = [

1221 "created",

1222 "paid_date",

1223 "timestamp",

1224 "msg_id",

1225 "file_name",

1226 "group_status",

1227 ]

1228

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 ]

1238

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 ""

1242

1243 paid_date_brief.short_description = _("paid date") # type: ignore

1244 paid_date_brief.admin_order_field = "paid_date" # type: ignore

1245

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)

1254

1255

1256 class 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 = ()

1275

1276 list_display = (

1277 "id",

1278 "name",

1279 "account_number",

1280 "bic",

1281 "org_id",

1282 "address",

1283 "country_code",

1284 )

1285

1286 raw_id_fields = ("payouts_account",)

1287

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

1294

1295

1296 class 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]

1329

1330

1331 class CurrencyExchangeSourceAdmin(BankAdminBase):

1332 save_on_top = False

1333 exclude = ()

1334

1335 fields = (

1336 "id",

1337 "created",

1338 "name",

1339 )

1340

1341 readonly_fields = (

1342 "id",

1343 "created",

1344 )

1345

1346 list_display = fields

1347

1348

1349 class CurrencyExchangeAdmin(BankAdminBase):

1350 save_on_top = False

1351

1352 fields = (

1353 "record_date",

1354 "source_currency",

1355 "target_currency",

1356 "unit_currency",

1357 "exchange_rate",

1358 "source",

1359 )

1360

1361 date_hierarchy = "record_date"

1362 readonly_fields = list_display = fields

1363 raw_id_fields = ("source",)

1364 list_filter = ("source_currency", "target_currency", "source")

1365

1366

1367 class WsEdiConnectionAdmin(BankAdminBase):

1368 save_on_top = False

1369

1370 ordering = [

1371 "name",

1372 ]

1373

1374 list_display = (

1375 "id",

1376 "created",

1377 "name",

1378 "sender_identifier",

1379 "receiver_identifier",

1380 "expires",

1381 )

1382

1383 raw_id_fields = ()

1384

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 )

1428

1429 readonly_fields = (

1430 "id",

1431 "created",

1432 "expires",

1433 )

1434

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 ""

1456

1457 expires.short_description = _("expires") # type: ignore

1458

1459

1460 class WsEdiSoapCallAdmin(BankAdminBase):

1461 save_on_top = False

1462

1463 date_hierarchy = "created"

1464

1465 list_display = (

1466 "id",

1467 "created",

1468 "connection",

1469 "command",

1470 "executed",

1471 "execution_time",

1472 )

1473

1474 list_filter = (

1475 "connection",

1476 "command",

1477 )

1478

1479 raw_id_fields = ()

1480

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 )

1493

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 )

1506

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

1512

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)))

1525

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()

1531

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)))

1538

1539 admin_application_request.short_description = _("application request") # type: ignore

1540

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)))

1547

1548 admin_application_response.short_description = _("application response") # type: ignore

1549

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)))

1557

1558 admin_application_response_file.short_description = _("file") # type: ignore

1559

1560 def execution_time(self, obj):

1561 assert isinstance(obj, WsEdiSoapCall)

1562 return obj.executed - obj.created if obj.executed else ""

1563

1564 execution_time.short_description = _("execution time") # type: ignore

1565

1566 def error_fmt(self, obj):

1567 assert isinstance(obj, WsEdiSoapCall)

1568 return mark_safe(obj.error.replace("\n", "<br>"))

1569

1570 error_fmt.short_description = _("error") # type: ignore

1571

1572

1573 class 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 ]

1595

1596

1597 class 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

1628

1629 def has_add_permission(self, request) -> bool: # type: ignore # noqa

1630 return False

1631

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)

1636

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)

1641

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)

1646

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)

1651

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")

1656

1657

1658 mark_as_manually_settled.short_description = _("Mark as manually settled") # type: ignore

1659 unmark_manually_settled_flag.short_description = _("Unmark manually settled flag") # type: ignore

1660 send_payouts_to_bank.short_description = _("Send payouts to bank") # type: ignore

1661 mark_payouts_as_paid.short_description = _("Mark payouts as paid") # type: ignore

1662 regenerate_payout_message_identifiers.short_description = _("Regenerate payout message identifiers") # type: ignore

1663

1664 admin.site.register(CurrencyExchangeSource, CurrencyExchangeSourceAdmin)

1665 admin.site.register(CurrencyExchange, CurrencyExchangeAdmin)

1666 admin.site.register(Payout, PayoutAdmin)

1667 admin.site.register(PayoutStatus, PayoutStatusAdmin)

1668 admin.site.register(PayoutParty, PayoutPartyAdmin)

1669 admin.site.register(Refund, RefundAdmin)

1670 admin.site.register(Statement, StatementAdmin)

1671 admin.site.register(StatementRecord, StatementRecordAdmin)

1672 admin.site.register(StatementFile, StatementFileAdmin)

1673 admin.site.register(ReferencePaymentRecord, ReferencePaymentRecordAdmin)

1674 admin.site.register(ReferencePaymentBatch, ReferencePaymentBatchAdmin)

1675 admin.site.register(ReferencePaymentBatchFile, ReferencePaymentBatchFileAdmin)

1676 admin.site.register(WsEdiConnection, WsEdiConnectionAdmin)

1677 admin.site.register(WsEdiSoapCall, WsEdiSoapCallAdmin)

1678 admin.site.register(EuriborRate, EuriborRateAdmin)

1679 admin.site.register(AccountBalance, AccountBalanceAdmin)