Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# pylint: disable=too-many-arguments 

2import base64 

3import logging 

4import os 

5import traceback 

6from datetime import datetime 

7from os.path import basename 

8from typing import Optional, Sequence 

9import pytz 

10from django import forms 

11from django.conf import settings 

12from django.conf.urls import url 

13from django.contrib import admin 

14from django.contrib import messages 

15from django.contrib.admin import SimpleListFilter 

16from django.contrib.auth.models import User 

17from django.contrib.messages import add_message, ERROR 

18from django.core.exceptions import ValidationError 

19from django.core.files.uploadedfile import InMemoryUploadedFile 

20from django.db import transaction 

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

22from django.db.models.aggregates import Sum 

23from django.http import HttpRequest, Http404 

24from django.shortcuts import render, get_object_or_404 

25from django.urls import ResolverMatch, reverse 

26from django.utils.formats import date_format, localize 

27from django.utils.html import format_html 

28from django.utils.safestring import mark_safe 

29from django.utils.text import capfirst 

30from django.utils.translation import gettext_lazy as _ 

31from jacc.admin import AccountEntryNoteInline, AccountEntryNoteAdmin 

32from jacc.models import Account, EntryType, AccountEntryNote 

33from jbank.x509_helpers import get_x509_cert_from_file 

34from jutil.request import get_ip 

35from jutil.responses import FormattedXmlResponse, FormattedXmlFileResponse 

36from jutil.xml import xml_to_dict 

37from jbank.helpers import create_statement, create_reference_payment_batch 

38from jbank.models import ( 

39 Statement, 

40 StatementRecord, 

41 StatementRecordSepaInfo, 

42 ReferencePaymentRecord, 

43 ReferencePaymentBatch, 

44 StatementFile, 

45 ReferencePaymentBatchFile, 

46 Payout, 

47 Refund, 

48 PayoutStatus, 

49 PayoutParty, 

50 StatementRecordDetail, 

51 StatementRecordRemittanceInfo, 

52 CurrencyExchange, 

53 CurrencyExchangeSource, 

54 WsEdiConnection, 

55 WsEdiSoapCall, 

56) 

57from jbank.tito import parse_tiliote_statements_from_file, parse_tiliote_statements 

58from jbank.svm import parse_svm_batches_from_file, parse_svm_batches 

59from jutil.admin import ModelAdminBase, admin_log, admin_log_changed_fields 

60 

61logger = logging.getLogger(__name__) 

62 

63 

64class BankAdminBase(ModelAdminBase): 

65 def save_form(self, request, form, change): 

66 if change: 

67 admin_log_changed_fields(form.instance, form.changed_data, request.user, ip=get_ip(request)) 

68 return form.save(commit=False) 

69 

70 def save_formset(self, request, form, formset, change): 

71 if formset.model == AccountEntryNote: 

72 AccountEntryNoteAdmin.save_account_entry_note_formset(request, form, formset, change) 

73 else: 

74 formset.save() 

75 

76 @staticmethod 

77 def format_admin_obj_link_list(qs: QuerySet, route: str): 

78 out = "" 

79 for e_id in list(qs.order_by("id").values_list("id", flat=True)): 

80 if out: 

81 out += " | " 

82 url_path = reverse(route, args=[e_id]) 

83 out += f'<a href="{url_path}">id={e_id}</a>' 

84 return mark_safe(out) 

85 

86 

87class SettlementEntryTypesFilter(SimpleListFilter): 

88 """ 

89 Filters incoming settlement type entries. 

90 """ 

91 

92 title = _("account entry types") 

93 parameter_name = "type" 

94 

95 def lookups(self, request, model_admin): 

96 choices = [] 

97 for e in EntryType.objects.all().filter(is_settlement=True).order_by("name"): 

98 assert isinstance(e, EntryType) 

99 choices.append((e.id, capfirst(e.name))) 

100 return choices 

101 

102 def queryset(self, request, queryset): 

103 val = self.value() 

104 if val: 

105 return queryset.filter(type__id=val) 

106 return queryset 

107 

108 

109class AccountEntryMatchedFilter(SimpleListFilter): 

110 """ 

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

112 """ 

113 

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

115 parameter_name = "matched" 

116 

117 def lookups(self, request, model_admin): 

118 return [ 

119 ("1", capfirst(_("account.entry.not.matched"))), 

120 ("2", capfirst(_("account.entry.is.matched"))), 

121 ("4", capfirst(_("not marked as settled"))), 

122 ("3", capfirst(_("marked as settled"))), 

123 ] 

124 

125 def queryset(self, request, queryset): 

126 val = self.value() 

127 if val: 

128 # return original settlements only 

129 queryset = queryset.filter(type__is_settlement=True, parent=None) 

130 if val == "1": 

131 # return those which are not manually settled and 

132 # have either a) no children b) sum of children less than amount 

133 queryset = queryset.exclude(manually_settled=True) 

134 queryset = queryset.annotate(child_set_amount=Sum("child_set__amount")) 

135 return queryset.filter(Q(child_set=None) | Q(child_set_amount__lt=F("amount"))) 

136 if val == "2": 

137 # return any entries with derived account entries or marked as manually settled 

138 return queryset.exclude(Q(child_set=None) & Q(manually_settled=False)) 

139 if val == "3": 

140 # return only manually marked as settled 

141 return queryset.filter(manually_settled=True) 

142 if val == "4": 

143 # return everything but manually marked as settled 

144 return queryset.filter(manually_settled=False) 

145 return queryset 

146 

147 

148class AccountNameFilter(SimpleListFilter): 

149 """ 

150 Filters account entries based on account name. 

151 """ 

152 

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

154 parameter_name = "account-name" 

155 

156 def lookups(self, request, model_admin): 

157 ops = [] 

158 qs = model_admin.get_queryset(request) 

159 for e in qs.distinct("account__name"): 

160 ops.append((e.account.name, e.account.name)) 

161 return sorted(ops, key=lambda x: x[0]) 

162 

163 def queryset(self, request, queryset): 

164 val = self.value() 

165 if val: 

166 return queryset.filter(account__name=val) 

167 return queryset 

168 

169 

170class StatementAdmin(BankAdminBase): 

171 exclude = () 

172 list_per_page = 20 

173 save_on_top = False 

174 ordering = ("-record_date", "account_number") 

175 date_hierarchy = "record_date" 

176 list_filter = ("account_number",) 

177 readonly_fields = ( 

178 "file_link", 

179 "account_number", 

180 "statement_number", 

181 "begin_date", 

182 "end_date", 

183 "record_date", 

184 "customer_identifier", 

185 "begin_balance_date", 

186 "begin_balance", 

187 "record_count", 

188 "currency_code", 

189 "account_name", 

190 "account_limit", 

191 "owner_name", 

192 "contact_info_1", 

193 "contact_info_2", 

194 "bank_specific_info_1", 

195 "iban", 

196 "bic", 

197 ) 

198 fields = readonly_fields 

199 search_fields = ( 

200 "name", 

201 "statement_number", 

202 ) 

203 list_display = ( 

204 "id", 

205 "record_date_short", 

206 "account_number", 

207 "statement_number", 

208 "begin_balance", 

209 "currency_code", 

210 "file_link", 

211 "account_entry_list", 

212 ) 

213 

214 def record_date_short(self, obj): 

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

216 

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

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

219 

220 def account_entry_list(self, obj): 

221 assert isinstance(obj, Statement) 

222 admin_url = reverse("admin:jbank_statementrecord_statement_changelist", args=(obj.id,)) 

223 return format_html( 

224 "<a href='{}'>{}</a>", mark_safe(admin_url), StatementRecord.objects.filter(statement=obj).count() 

225 ) 

226 

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

228 

229 def file_link(self, obj): 

230 assert isinstance(obj, Statement) 

231 if not obj.file: 

232 return "" 

233 admin_url = reverse("admin:jbank_statementfile_change", args=(obj.file.id,)) 

234 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.name) 

235 

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

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

238 

239 def get_urls(self): 

240 return [ 

241 url( 

242 r"^by-file/(?P<file_id>\d+)/$", 

243 self.admin_site.admin_view(self.kw_changelist_view), 

244 name="jbank_statement_file_changelist", 

245 ), 

246 ] + super().get_urls() 

247 

248 def get_queryset(self, request: HttpRequest): 

249 rm = request.resolver_match 

250 assert isinstance(rm, ResolverMatch) 

251 qs = super().get_queryset(request) 

252 file_id = rm.kwargs.get("file_id", None) 

253 if file_id: 

254 qs = qs.filter(file=file_id) 

255 return qs 

256 

257 

258class StatementRecordDetailInlineAdmin(admin.StackedInline): 

259 exclude = () 

260 model = StatementRecordDetail 

261 can_delete = False 

262 extra = 0 

263 

264 fields = ( 

265 "batch_identifier", 

266 "amount", 

267 "creditor_account", 

268 "creditor_account_scheme", 

269 "currency_code", 

270 "instructed_amount", 

271 "exchange", 

272 "archive_identifier", 

273 "end_to_end_identifier", 

274 "creditor_name", 

275 "debtor_name", 

276 "ultimate_debtor_name", 

277 "unstructured_remittance_info", 

278 "paid_date", 

279 "structured_remittance_info", 

280 ) 

281 readonly_fields = fields 

282 raw_id_fields = () 

283 

284 def structured_remittance_info(self, obj): 

285 assert isinstance(obj, StatementRecordDetail) 

286 lines = [] 

287 for rinfo in obj.remittanceinfo_set.all().order_by("id"): 

288 assert isinstance(rinfo, StatementRecordRemittanceInfo) 

289 lines.append(str(rinfo)) 

290 return mark_safe("<br>".join(lines)) 

291 

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

293 

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

295 return False 

296 

297 

298class StatementRecordSepaInfoInlineAdmin(admin.StackedInline): 

299 exclude = () 

300 model = StatementRecordSepaInfo 

301 can_delete = False 

302 extra = 0 

303 max_num = 1 

304 

305 readonly_fields = ( 

306 "record", 

307 "reference", 

308 "iban_account_number", 

309 "bic_code", 

310 "recipient_name_detail", 

311 "payer_name_detail", 

312 "identifier", 

313 "archive_identifier", 

314 ) 

315 raw_id_fields = ("record",) 

316 

317 def has_add_permission(self, request, obj=None): # pylint: disable=unused-argument 

318 return False 

319 

320 

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

322 try: 

323 data = request.POST.dict() 

324 

325 if "description" in data: 

326 description = data["description"] 

327 user = request.user 

328 for e in list(qs.filter(manually_settled=False)): 

329 e.manually_settled = True 

330 e.save(update_fields=["manually_settled"]) 

331 msg = "{}: {}".format(capfirst(_("marked as manually settled")), description) 

332 admin_log([e], msg, who=user) 

333 messages.info(request, msg) 

334 else: 

335 cx = { 

336 "qs": qs, 

337 } 

338 return render(request, "admin/jbank/mark_as_manually_settled.html", context=cx) 

339 except ValidationError as e: 

340 messages.error(request, " ".join(e.messages)) 

341 except Exception as e: 

342 logger.error("mark_as_manually_settled: %s", traceback.format_exc()) 

343 messages.error(request, "{}".format(e)) 

344 return None 

345 

346 

347def unmark_manually_settled_flag(modeladmin, request, qs): # pylint: disable=unused-argument 

348 user = request.user 

349 for e in list(qs.filter(manually_settled=True)): 

350 e.manually_settled = False 

351 e.save(update_fields=["manually_settled"]) 

352 msg = capfirst(_("manually settled flag cleared")) 

353 admin_log([e], msg, who=user) 

354 messages.info(request, msg) 

355 

356 

357class StatementRecordAdmin(BankAdminBase): 

358 list_per_page = 25 

359 save_on_top = False 

360 date_hierarchy = "record_date" 

361 fields = ( 

362 "id", 

363 "entry_type", 

364 "statement", 

365 "line_number", 

366 "file_link", 

367 "record_number", 

368 "archive_identifier", 

369 "record_date", 

370 "value_date", 

371 "paid_date", 

372 "type", 

373 "record_code", 

374 "record_domain", 

375 "family_code", 

376 "sub_family_code", 

377 "record_description", 

378 "amount", 

379 "receipt_code", 

380 "delivery_method", 

381 "name", 

382 "name_source", 

383 "recipient_account_number", 

384 "recipient_account_number_changed", 

385 "remittance_info", 

386 "messages", 

387 "client_messages", 

388 "bank_messages", 

389 "account", 

390 "timestamp", 

391 "created", 

392 "last_modified", 

393 "description", 

394 "source_file", 

395 "archived", 

396 "manually_settled", 

397 "is_settled_bool", 

398 "child_links", 

399 ) 

400 readonly_fields = fields 

401 raw_id_fields = ( 

402 "statement", 

403 # from AccountEntry 

404 "account", 

405 "source_file", 

406 "parent", 

407 "source_invoice", 

408 "settled_invoice", 

409 "settled_item", 

410 ) 

411 list_filter = ( 

412 "statement__file__tag", 

413 AccountNameFilter, 

414 "manually_settled", 

415 SettlementEntryTypesFilter, 

416 "record_code", 

417 ) 

418 search_fields = ( 

419 "=archive_identifier", 

420 "=amount", 

421 "=recipient_account_number", 

422 "record_description", 

423 "name", 

424 "remittance_info", 

425 "messages", 

426 ) 

427 list_display = ( 

428 "id", 

429 "record_date_short", 

430 "type", 

431 "record_code", 

432 "amount", 

433 "name", 

434 "source_file_link", 

435 "is_settled_bool", 

436 ) 

437 inlines = ( 

438 StatementRecordSepaInfoInlineAdmin, 

439 StatementRecordDetailInlineAdmin, 

440 AccountEntryNoteInline, 

441 ) 

442 actions = ( 

443 mark_as_manually_settled, 

444 unmark_manually_settled_flag, 

445 ) 

446 

447 def is_settled_bool(self, obj): 

448 return obj.is_settled 

449 

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

451 is_settled_bool.boolean = True # type: ignore 

452 

453 def record_date_short(self, obj): 

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

455 

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

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

458 

459 def child_links(self, obj) -> str: 

460 assert isinstance(obj, StatementRecord) 

461 return self.format_admin_obj_link_list(obj.child_set, "admin:jacc_accountentry_change") 

462 

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

464 

465 def get_urls(self): 

466 return [ 

467 url( 

468 r"^by-statement/(?P<statement_id>\d+)/$", 

469 self.admin_site.admin_view(self.kw_changelist_view), 

470 name="jbank_statementrecord_statement_changelist", 

471 ), 

472 url( 

473 r"^by-statement-file/(?P<statement_file_id>\d+)/$", 

474 self.admin_site.admin_view(self.kw_changelist_view), 

475 name="jbank_statementrecord_statementfile_changelist", 

476 ), 

477 ] + super().get_urls() 

478 

479 def get_queryset(self, request: HttpRequest): 

480 rm = request.resolver_match 

481 assert isinstance(rm, ResolverMatch) 

482 qs = super().get_queryset(request) 

483 statement_id = rm.kwargs.get("statement_id", None) 

484 if statement_id: 

485 qs = qs.filter(statement__id=statement_id) 

486 statement_file_id = rm.kwargs.get("statement_file_id", None) 

487 if statement_file_id: 

488 qs = qs.filter(statement__file_id=statement_file_id) 

489 return qs 

490 

491 def source_file_link(self, obj): 

492 assert isinstance(obj, StatementRecord) 

493 if not obj.statement: 

494 return "" 

495 admin_url = reverse("admin:jbank_statementfile_change", args=(obj.statement.file.id,)) 

496 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.statement.name) 

497 

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

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

500 

501 def file_link(self, obj): 

502 assert isinstance(obj, StatementRecord) 

503 if not obj.statement or not obj.statement.file: 

504 return "" 

505 name = basename(obj.statement.file.file.name) 

506 admin_url = reverse("admin:jbank_statementfile_change", args=(obj.statement.file.id,)) 

507 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), name) 

508 

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

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

511 

512 

513class ReferencePaymentRecordAdmin(BankAdminBase): 

514 exclude = () 

515 list_per_page = 25 

516 save_on_top = False 

517 date_hierarchy = "record_date" 

518 raw_id_fields = ( 

519 "batch", 

520 # from AccountEntry 

521 "account", 

522 "source_file", 

523 "parent", 

524 "source_invoice", 

525 "settled_invoice", 

526 "settled_item", 

527 ) 

528 fields = [ 

529 "id", 

530 "batch", 

531 "line_number", 

532 "file_link", 

533 "record_type", 

534 "account_number", 

535 "record_date", 

536 "paid_date", 

537 "archive_identifier", 

538 "remittance_info", 

539 "payer_name", 

540 "currency_identifier", 

541 "name_source", 

542 "amount", 

543 "correction_identifier", 

544 "delivery_method", 

545 "receipt_code", 

546 "archived", 

547 "account", 

548 "created", 

549 "last_modified", 

550 "timestamp", 

551 "type", 

552 "description", 

553 "manually_settled", 

554 "is_settled_bool", 

555 "child_links", 

556 ] 

557 readonly_fields = ( 

558 "id", 

559 "batch", 

560 "line_number", 

561 "file_link", 

562 "record_type", 

563 "account_number", 

564 "record_date", 

565 "paid_date", 

566 "archive_identifier", 

567 "remittance_info", 

568 "payer_name", 

569 "currency_identifier", 

570 "name_source", 

571 "amount", 

572 "correction_identifier", 

573 "delivery_method", 

574 "receipt_code", 

575 "archived", 

576 "manually_settled", 

577 "account", 

578 "timestamp", 

579 "created", 

580 "last_modified", 

581 "type", 

582 "description", 

583 "amount", 

584 "source_file", 

585 "source_invoice", 

586 "settled_invoice", 

587 "settled_item", 

588 "parent", 

589 "is_settled_bool", 

590 "child_links", 

591 ) 

592 list_filter = ( 

593 "batch__file__tag", 

594 AccountNameFilter, 

595 AccountEntryMatchedFilter, 

596 "correction_identifier", 

597 ) 

598 search_fields = ( 

599 "=archive_identifier", 

600 "=amount", 

601 "remittance_info", 

602 "payer_name", 

603 "batch__name", 

604 ) 

605 list_display = ( 

606 "id", 

607 "record_date", 

608 "type", 

609 "amount", 

610 "payer_name", 

611 "remittance_info", 

612 "source_file_link", 

613 "is_settled_bool", 

614 ) 

615 actions = ( 

616 mark_as_manually_settled, 

617 unmark_manually_settled_flag, 

618 ) 

619 inlines = [ 

620 AccountEntryNoteInline, 

621 ] 

622 

623 def is_settled_bool(self, obj): 

624 return obj.is_settled 

625 

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

627 is_settled_bool.boolean = True # type: ignore 

628 

629 def record_date_short(self, obj): 

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

631 

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

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

634 

635 def child_links(self, obj) -> str: 

636 assert isinstance(obj, ReferencePaymentRecord) 

637 return self.format_admin_obj_link_list(obj.child_set, "admin:jacc_accountentry_change") 

638 

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

640 

641 def file_link(self, obj): 

642 assert isinstance(obj, ReferencePaymentRecord) 

643 if not obj.batch or not obj.batch.file: 

644 return "" 

645 admin_url = reverse("admin:jbank_referencepaymentbatchfile_change", args=(obj.batch.file.id,)) 

646 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.batch.file) 

647 

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

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

650 

651 def get_urls(self): 

652 return [ 

653 url( 

654 r"^by-batch/(?P<batch_id>\d+)/$", 

655 self.admin_site.admin_view(self.kw_changelist_view), 

656 name="jbank_referencepaymentrecord_batch_changelist", 

657 ), 

658 url( 

659 r"^by-statement-file/(?P<stm_id>\d+)/$", 

660 self.admin_site.admin_view(self.kw_changelist_view), 

661 name="jbank_referencepaymentrecord_statementfile_changelist", 

662 ), 

663 ] + super().get_urls() 

664 

665 def get_queryset(self, request: HttpRequest): 

666 rm = request.resolver_match 

667 assert isinstance(rm, ResolverMatch) 

668 qs = super().get_queryset(request) 

669 batch_id = rm.kwargs.get("batch_id", None) 

670 if batch_id: 

671 qs = qs.filter(batch_id=batch_id) 

672 stm_id = rm.kwargs.get("stm_id", None) 

673 if stm_id: 

674 qs = qs.filter(batch__file_id=stm_id) 

675 return qs 

676 

677 def source_file_link(self, obj): 

678 assert isinstance(obj, ReferencePaymentRecord) 

679 if not obj.batch: 

680 return "" 

681 admin_url = reverse("admin:jbank_referencepaymentbatchfile_change", args=(obj.batch.file.id,)) 

682 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.batch.name) 

683 

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

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

686 

687 

688class ReferencePaymentBatchAdmin(BankAdminBase): 

689 exclude = () 

690 list_per_page = 20 

691 save_on_top = False 

692 ordering = ("-record_date",) 

693 date_hierarchy = "record_date" 

694 list_filter = ("record_set__account_number",) 

695 fields = ( 

696 "file_link", 

697 "record_date", 

698 "institution_identifier", 

699 "service_identifier", 

700 "currency_identifier", 

701 ) 

702 readonly_fields = ( 

703 "name", 

704 "file", 

705 "file_link", 

706 "record_date", 

707 "institution_identifier", 

708 "service_identifier", 

709 "currency_identifier", 

710 ) 

711 search_fields = ( 

712 "name", 

713 "=record_set__archive_identifier", 

714 "=record_set__amount", 

715 "record_set__remittance_info", 

716 "record_set__payer_name", 

717 ) 

718 list_display = ( 

719 "id", 

720 "name", 

721 "record_date_short", 

722 "service_identifier", 

723 "currency_identifier", 

724 "account_entry_list", 

725 ) 

726 

727 def record_date_short(self, obj): 

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

729 

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

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

732 

733 def account_entry_list(self, obj): 

734 assert isinstance(obj, ReferencePaymentBatch) 

735 admin_url = reverse("admin:jbank_referencepaymentrecord_batch_changelist", args=(obj.id,)) 

736 return format_html( 

737 "<a href='{}'>{}</a>", mark_safe(admin_url), ReferencePaymentRecord.objects.filter(batch=obj).count() 

738 ) 

739 

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

741 

742 def file_link(self, obj): 

743 assert isinstance(obj, ReferencePaymentBatch) 

744 if not obj.file: 

745 return "" 

746 admin_url = reverse("admin:jbank_referencepaymentbatchfile_change", args=(obj.file.id,)) 

747 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.file) 

748 

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

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

751 

752 def get_urls(self): 

753 return [ 

754 url( 

755 r"^by-file/(?P<file_id>\d+)/$", 

756 self.admin_site.admin_view(self.kw_changelist_view), 

757 name="jbank_referencepaymentbatch_file_changelist", 

758 ), 

759 ] + super().get_urls() 

760 

761 def get_queryset(self, request: HttpRequest): 

762 rm = request.resolver_match 

763 assert isinstance(rm, ResolverMatch) 

764 qs = super().get_queryset(request) 

765 file_id = rm.kwargs.get("file_id", None) 

766 if file_id: 

767 qs = qs.filter(file=file_id) 

768 return qs 

769 

770 

771class StatementFileForm(forms.ModelForm): 

772 class Meta: 

773 fields = [ 

774 "file", 

775 ] 

776 

777 def clean_file(self): 

778 file = self.cleaned_data["file"] 

779 assert isinstance(file, InMemoryUploadedFile) 

780 name = file.name 

781 file.seek(0) 

782 content = file.read() 

783 assert isinstance(content, bytes) 

784 try: 

785 statements = parse_tiliote_statements(content.decode("ISO-8859-1"), filename=basename(name)) 

786 for stm in statements: 

787 account_number = stm["header"]["account_number"] 

788 if Account.objects.filter(name=account_number).count() == 0: 

789 raise ValidationError(_("account.not.found").format(account_number=account_number)) 

790 except ValidationError: 

791 raise 

792 except Exception as e: 

793 raise ValidationError(_("Unhandled error") + ": {}".format(e)) 

794 return file 

795 

796 

797class StatementFileAdmin(BankAdminBase): 

798 save_on_top = False 

799 exclude = () 

800 form = StatementFileForm 

801 

802 date_hierarchy = "created" 

803 

804 search_fields = ("original_filename__contains",) 

805 

806 list_filter = ("tag",) 

807 

808 list_display = ( 

809 "id", 

810 "created", 

811 "file", 

812 "records", 

813 ) 

814 

815 readonly_fields = ( 

816 "created", 

817 "errors", 

818 "file", 

819 "original_filename", 

820 "records", 

821 ) 

822 

823 def records(self, obj): 

824 assert isinstance(obj, StatementFile) 

825 url = reverse("admin:jbank_statementrecord_statementfile_changelist", args=[obj.id]) 

826 return format_html('<a href="{}">{}</a>', url, _("statement records")) 

827 

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

829 

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

831 return False 

832 

833 def construct_change_message(self, request, form, formsets, add=False): 

834 if add: 

835 instance = form.instance 

836 assert isinstance(instance, StatementFile) 

837 if instance.file: 

838 full_path = instance.full_path 

839 plain_filename = basename(full_path) 

840 try: 

841 statements = parse_tiliote_statements_from_file(full_path) 

842 with transaction.atomic(): 

843 for data in statements: 

844 create_statement(data, name=plain_filename, file=instance) 

845 except Exception as e: 

846 instance.errors = traceback.format_exc() 

847 instance.save() 

848 add_message(request, ERROR, str(e)) 

849 instance.delete() 

850 

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

852 

853 

854class ReferencePaymentBatchFileForm(forms.ModelForm): 

855 class Meta: 

856 fields = [ 

857 "file", 

858 ] 

859 

860 def clean_file(self): 

861 file = self.cleaned_data["file"] 

862 assert isinstance(file, InMemoryUploadedFile) 

863 name = file.name 

864 file.seek(0) 

865 content = file.read() 

866 assert isinstance(content, bytes) 

867 try: 

868 batches = parse_svm_batches(content.decode("ISO-8859-1"), filename=basename(name)) 

869 for b in batches: 

870 for rec in b["records"]: 

871 account_number = rec["account_number"] 

872 if Account.objects.filter(name=account_number).count() == 0: 

873 raise ValidationError(_("account.not.found").format(account_number=account_number)) 

874 except ValidationError: 

875 raise 

876 except Exception as e: 

877 raise ValidationError(_("Unhandled error") + ": {}".format(e)) 

878 return file 

879 

880 

881class ReferencePaymentBatchFileAdmin(BankAdminBase): 

882 save_on_top = False 

883 exclude = () 

884 form = ReferencePaymentBatchFileForm 

885 date_hierarchy = "created" 

886 

887 list_display = ( 

888 "id", 

889 "created", 

890 "file", 

891 "total", 

892 ) 

893 

894 list_filter = ("tag",) 

895 

896 search_fields = ("file__contains",) 

897 

898 readonly_fields = ( 

899 "created", 

900 "errors", 

901 "file", 

902 "original_filename", 

903 ) 

904 

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

906 return False 

907 

908 def total(self, obj): 

909 assert isinstance(obj, ReferencePaymentBatchFile) 

910 path = reverse("admin:jbank_referencepaymentrecord_statementfile_changelist", args=[obj.id]) 

911 return format_html('<a href="{}">{}</a>', path, localize(obj.total_amount)) 

912 

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

914 

915 def construct_change_message(self, request, form, formsets, add=False): 

916 if add: 

917 instance = form.instance 

918 assert isinstance(instance, ReferencePaymentBatchFile) 

919 if instance.file: 

920 full_path = instance.full_path 

921 plain_filename = basename(full_path) 

922 try: 

923 batches = parse_svm_batches_from_file(full_path) 

924 with transaction.atomic(): 

925 for data in batches: 

926 create_reference_payment_batch(data, name=plain_filename, file=instance) 

927 except Exception as e: 

928 user = request.user 

929 assert isinstance(user, User) 

930 instance.errors = traceback.format_exc() 

931 instance.save() 

932 msg = str(e) 

933 if user.is_superuser: 

934 msg = instance.errors 

935 logger.error("%s: %s", plain_filename, msg) 

936 add_message(request, ERROR, msg) 

937 instance.delete() 

938 

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

940 

941 

942class PayoutStatusAdmin(BankAdminBase): 

943 fields = ( 

944 "created", 

945 "payout", 

946 "file_name_link", 

947 "response_code", 

948 "response_text", 

949 "msg_id", 

950 "original_msg_id", 

951 "group_status", 

952 "status_reason", 

953 ) 

954 readonly_fields = fields 

955 list_display = ( 

956 "id", 

957 "created", 

958 "payout", 

959 "file_name_link", 

960 "response_code", 

961 "response_text", 

962 "original_msg_id", 

963 "group_status", 

964 ) 

965 

966 def file_download_view( 

967 self, request, pk, filename, form_url="", extra_context=None 

968 ): # pylint: disable=unused-argument 

969 user = request.user 

970 if not user.is_authenticated or not user.is_staff: 

971 raise Http404(_("File {} not found").format(filename)) 

972 obj = get_object_or_404(self.get_queryset(request), pk=pk, file_name=filename) 

973 assert isinstance(obj, PayoutStatus) 

974 full_path = obj.full_path 

975 if not os.path.isfile(full_path): 

976 raise Http404(_("File {} not found").format(filename)) 

977 return FormattedXmlFileResponse(full_path) 

978 

979 def file_name_link(self, obj): 

980 assert isinstance(obj, PayoutStatus) 

981 if obj.id is None or not obj.full_path: 

982 return obj.file_name 

983 admin_url = reverse( 

984 "admin:jbank_payoutstatus_file_download", 

985 args=( 

986 obj.id, 

987 obj.file_name, 

988 ), 

989 ) 

990 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.file_name) 

991 

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

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

994 

995 def get_urls(self): 

996 urls = [ 

997 url( 

998 r"^(\d+)/change/status-downloads/(.+)/$", 

999 self.file_download_view, 

1000 name="jbank_payoutstatus_file_download", 

1001 ), 

1002 ] 

1003 return urls + super().get_urls() 

1004 

1005 

1006class PayoutStatusInlineAdmin(admin.TabularInline): 

1007 model = PayoutStatus 

1008 can_delete = False 

1009 extra = 0 

1010 ordering = ("-id",) 

1011 fields = PayoutStatusAdmin.fields 

1012 readonly_fields = PayoutStatusAdmin.readonly_fields 

1013 

1014 def file_name_link(self, obj): 

1015 assert isinstance(obj, PayoutStatus) 

1016 if obj.id is None or not obj.full_path: 

1017 return obj.file_name 

1018 admin_url = reverse( 

1019 "admin:jbank_payoutstatus_file_download", 

1020 args=( 

1021 obj.id, 

1022 obj.file_name, 

1023 ), 

1024 ) 

1025 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.file_name) 

1026 

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

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

1029 

1030 

1031class PayoutAdmin(BankAdminBase): 

1032 save_on_top = False 

1033 exclude = () 

1034 inlines = [PayoutStatusInlineAdmin, AccountEntryNoteInline] 

1035 date_hierarchy = "timestamp" 

1036 

1037 raw_id_fields: Sequence[str] = ( 

1038 "account", 

1039 "parent", 

1040 "payer", 

1041 "recipient", 

1042 ) 

1043 

1044 list_filter: Sequence[str] = ( 

1045 "state", 

1046 "payoutstatus_set__response_code", 

1047 "payoutstatus_set__group_status", 

1048 "recipient__bic", 

1049 ) 

1050 

1051 fields: Sequence[str] = ( 

1052 "connection", 

1053 "account", 

1054 "parent", 

1055 "payer", 

1056 "recipient", 

1057 "amount", 

1058 "messages", 

1059 "reference", 

1060 "due_date", 

1061 "msg_id", 

1062 "file_name", 

1063 "timestamp", 

1064 "paid_date", 

1065 "state", 

1066 "group_status", 

1067 "created", 

1068 ) 

1069 

1070 list_display: Sequence[str] = ( 

1071 "id", 

1072 "timestamp", 

1073 "recipient", 

1074 "amount", 

1075 "paid_date", 

1076 "state", 

1077 ) 

1078 

1079 readonly_fields: Sequence[str] = ( 

1080 "created", 

1081 "paid_date", 

1082 "timestamp", 

1083 "msg_id", 

1084 "file_name", 

1085 "group_status", 

1086 ) 

1087 

1088 search_fields: Sequence[str] = ( 

1089 "=msg_id", 

1090 "=file_name", 

1091 "=file_reference", 

1092 "recipient__name", 

1093 "=recipient__account_number", 

1094 "=msg_id", 

1095 "=amount", 

1096 ) 

1097 

1098 def save_model(self, request, obj, form, change): 

1099 assert isinstance(obj, Payout) 

1100 if not change: 

1101 if not hasattr(obj, "account") or not obj.account: 

1102 obj.account = obj.payer.payouts_account 

1103 if not hasattr(obj, "type") or not obj.type: 

1104 obj.type = EntryType.objects.get(code=settings.E_BANK_PAYOUT) 

1105 return super().save_model(request, obj, form, change) 

1106 

1107 

1108class PayoutPartyAdmin(BankAdminBase): 

1109 save_on_top = False 

1110 exclude = () 

1111 search_fields = ( 

1112 "name", 

1113 "=account_number", 

1114 "=org_id", 

1115 ) 

1116 ordering = ("name",) 

1117 

1118 actions = () 

1119 

1120 list_display = ( 

1121 "id", 

1122 "name", 

1123 "account_number", 

1124 "bic", 

1125 "org_id", 

1126 "address", 

1127 "country_code", 

1128 ) 

1129 

1130 raw_id_fields = ("payouts_account",) 

1131 

1132 

1133class RefundAdmin(PayoutAdmin): 

1134 raw_id_fields = ( 

1135 "account", 

1136 "parent", 

1137 "payer", 

1138 "recipient", 

1139 ) 

1140 fields = ( 

1141 "connection", 

1142 "account", 

1143 "payer", 

1144 "parent", 

1145 "recipient", 

1146 "amount", 

1147 "messages", 

1148 "reference", 

1149 "attachment", 

1150 "msg_id", 

1151 "file_name", 

1152 "timestamp", 

1153 "paid_date", 

1154 "group_status", 

1155 "created", 

1156 ) 

1157 readonly_fields = ( 

1158 "msg_id", 

1159 "file_name", 

1160 "timestamp", 

1161 "paid_date", 

1162 "group_status", 

1163 "created", 

1164 ) 

1165 inlines = [AccountEntryNoteInline] 

1166 

1167 

1168class CurrencyExchangeSourceAdmin(BankAdminBase): 

1169 save_on_top = False 

1170 exclude = () 

1171 

1172 fields = ( 

1173 "id", 

1174 "created", 

1175 "name", 

1176 ) 

1177 

1178 readonly_fields = ( 

1179 "id", 

1180 "created", 

1181 ) 

1182 

1183 list_display = fields 

1184 

1185 

1186class CurrencyExchangeAdmin(BankAdminBase): 

1187 save_on_top = False 

1188 

1189 fields = ( 

1190 "record_date", 

1191 "source_currency", 

1192 "target_currency", 

1193 "unit_currency", 

1194 "exchange_rate", 

1195 "source", 

1196 ) 

1197 

1198 date_hierarchy = "record_date" 

1199 readonly_fields = list_display = fields 

1200 raw_id_fields = ("source",) 

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

1202 

1203 

1204class WsEdiConnectionAdmin(BankAdminBase): 

1205 save_on_top = False 

1206 

1207 ordering = [ 

1208 "name", 

1209 ] 

1210 

1211 list_display = ( 

1212 "id", 

1213 "created", 

1214 "name", 

1215 "sender_identifier", 

1216 "receiver_identifier", 

1217 "expires", 

1218 ) 

1219 

1220 raw_id_fields = () 

1221 

1222 fieldsets = ( 

1223 ( 

1224 None, 

1225 { 

1226 "fields": [ 

1227 "id", 

1228 "name", 

1229 "enabled", 

1230 "sender_identifier", 

1231 "receiver_identifier", 

1232 "target_identifier", 

1233 "environment", 

1234 "debug_commands", 

1235 "created", 

1236 ] 

1237 }, 

1238 ), 

1239 ( 

1240 "PKI", 

1241 { 

1242 "fields": [ 

1243 "pki_endpoint", 

1244 "pin", 

1245 "bank_root_cert_file", 

1246 ] 

1247 }, 

1248 ), 

1249 ( 

1250 "EDI", 

1251 { 

1252 "fields": [ 

1253 "soap_endpoint", 

1254 "signing_cert_file", 

1255 "signing_key_file", 

1256 "encryption_cert_file", 

1257 "encryption_key_file", 

1258 "bank_encryption_cert_file", 

1259 "bank_signing_cert_file", 

1260 "ca_cert_file", 

1261 ] 

1262 }, 

1263 ), 

1264 ) 

1265 

1266 readonly_fields = ( 

1267 "id", 

1268 "created", 

1269 "expires", 

1270 ) 

1271 

1272 def expires(self, obj): 

1273 assert isinstance(obj, WsEdiConnection) 

1274 min_not_valid_after: Optional[datetime] = None 

1275 try: 

1276 certs = [ 

1277 obj.signing_cert_full_path, 

1278 obj.encryption_cert_full_path, 

1279 obj.bank_encryption_cert_full_path, 

1280 obj.bank_root_cert_full_path, 

1281 obj.ca_cert_full_path, 

1282 ] 

1283 except Exception as e: 

1284 logger.error(e) 

1285 return _("(missing certificate files)") 

1286 for filename in certs: 

1287 if filename and os.path.isfile(filename): 

1288 cert = get_x509_cert_from_file(filename) 

1289 not_valid_after = pytz.utc.localize(cert.not_valid_after) 

1290 if min_not_valid_after is None or not_valid_after < min_not_valid_after: 

1291 min_not_valid_after = not_valid_after 

1292 return date_format(min_not_valid_after.date(), "SHORT_DATE_FORMAT") if min_not_valid_after else "" 

1293 

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

1295 

1296 

1297class WsEdiSoapCallAdmin(BankAdminBase): 

1298 save_on_top = False 

1299 

1300 date_hierarchy = "created" 

1301 

1302 list_display = ( 

1303 "id", 

1304 "created", 

1305 "connection", 

1306 "command", 

1307 "executed", 

1308 "execution_time", 

1309 ) 

1310 

1311 list_filter = ( 

1312 "connection", 

1313 "command", 

1314 ) 

1315 

1316 raw_id_fields = () 

1317 

1318 fields = ( 

1319 "id", 

1320 "connection", 

1321 "command", 

1322 "created", 

1323 "executed", 

1324 "execution_time", 

1325 "error_fmt", 

1326 "admin_application_request", 

1327 "admin_application_response", 

1328 "admin_application_response_file", 

1329 ) 

1330 

1331 readonly_fields = ( 

1332 "id", 

1333 "connection", 

1334 "command", 

1335 "created", 

1336 "executed", 

1337 "execution_time", 

1338 "error_fmt", 

1339 "admin_application_request", 

1340 "admin_application_response", 

1341 "admin_application_response_file", 

1342 ) 

1343 

1344 def get_fields(self, request, obj=None): 

1345 fields = super().get_fields(request, obj) 

1346 if not request.user.is_superuser: 

1347 fields = fields[:-2] 

1348 return fields 

1349 

1350 def soap_download_view( 

1351 self, request, object_id, file_type, form_url="", extra_context=None 

1352 ): # pylint: disable=unused-argument 

1353 user = request.user 

1354 if not user.is_authenticated or not user.is_superuser: 

1355 raise Http404("File not found") 

1356 obj = get_object_or_404(self.get_queryset(request), id=object_id) 

1357 assert isinstance(obj, WsEdiSoapCall) 

1358 if file_type == "f": 

1359 with open(obj.debug_response_full_path, "rb") as fb: 

1360 data = xml_to_dict(fb.read()) 

1361 content = base64.b64decode(data.get("Content", "")) 

1362 return FormattedXmlResponse(content, filename=obj.debug_get_filename(file_type)) 

1363 return FormattedXmlFileResponse(WsEdiSoapCall.debug_get_file_path(obj.debug_get_filename(file_type))) 

1364 

1365 def admin_application_request(self, obj): 

1366 assert isinstance(obj, WsEdiSoapCall) 

1367 if not os.path.isfile(obj.debug_request_full_path): 

1368 return "" 

1369 download_url = reverse("admin:jbank_wsedisoapcall_soap_download", args=[str(obj.id), "q"]) 

1370 return mark_safe( 

1371 format_html('<a href="{}">{}</a>', download_url, os.path.basename(obj.debug_request_full_path)) 

1372 ) 

1373 

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

1375 

1376 def admin_application_response(self, obj): 

1377 assert isinstance(obj, WsEdiSoapCall) 

1378 if not os.path.isfile(obj.debug_response_full_path): 

1379 return "" 

1380 download_url = reverse("admin:jbank_wsedisoapcall_soap_download", args=[str(obj.id), "s"]) 

1381 return mark_safe( 

1382 format_html('<a href="{}">{}</a>', download_url, os.path.basename(obj.debug_response_full_path)) 

1383 ) 

1384 

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

1386 

1387 def admin_application_response_file(self, obj): 

1388 assert isinstance(obj, WsEdiSoapCall) 

1389 if obj.command != "DownloadFile" or not obj.executed: 

1390 return "" 

1391 file_type = "f" 

1392 download_url = reverse("admin:jbank_wsedisoapcall_soap_download", args=[str(obj.id), file_type]) 

1393 return mark_safe(format_html('<a href="{}">{}</a>', download_url, obj.debug_get_filename(file_type))) 

1394 

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

1396 

1397 def execution_time(self, obj): 

1398 assert isinstance(obj, WsEdiSoapCall) 

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

1400 

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

1402 

1403 def error_fmt(self, obj): 

1404 assert isinstance(obj, WsEdiSoapCall) 

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

1406 

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

1408 

1409 def get_urls(self): 

1410 info = self.model._meta.app_label, self.model._meta.model_name 

1411 return [ 

1412 url(r"^soap-download/(\d+)/(.+)$", self.soap_download_view, name="%s_%s_soap_download" % info), 

1413 ] + super().get_urls() 

1414 

1415 

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

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

1418 

1419admin.site.register(CurrencyExchangeSource, CurrencyExchangeSourceAdmin) 

1420admin.site.register(CurrencyExchange, CurrencyExchangeAdmin) 

1421admin.site.register(Payout, PayoutAdmin) 

1422admin.site.register(PayoutStatus, PayoutStatusAdmin) 

1423admin.site.register(PayoutParty, PayoutPartyAdmin) 

1424admin.site.register(Refund, RefundAdmin) 

1425admin.site.register(Statement, StatementAdmin) 

1426admin.site.register(StatementRecord, StatementRecordAdmin) 

1427admin.site.register(StatementFile, StatementFileAdmin) 

1428admin.site.register(ReferencePaymentRecord, ReferencePaymentRecordAdmin) 

1429admin.site.register(ReferencePaymentBatch, ReferencePaymentBatchAdmin) 

1430admin.site.register(ReferencePaymentBatchFile, ReferencePaymentBatchFileAdmin) 

1431admin.site.register(WsEdiConnection, WsEdiConnectionAdmin) 

1432admin.site.register(WsEdiSoapCall, WsEdiSoapCallAdmin)