Coverage for jbank/admin.py: 48%

794 statements  

« 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 

69 

70logger = logging.getLogger(__name__) 

71 

72 

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

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

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

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

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 ) 

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 

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

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

352def 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 

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

387 

388 

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

406 

407 

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 ) 

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 

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 ] 

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 

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 ) 

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 

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

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

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

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

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

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 ) 

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 

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 

1111 

1112 

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

1129 

1130 

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

1145 

1146 

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

1165 

1166 

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

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

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 

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] 

1329 

1330 

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

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

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

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

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 ] 

1595 

1596 

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 

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 

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 

1663 

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)