Coverage for jbank/admin.py: 51%

# Coverage for jbank/admin.py : 51%

## 646 statements

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

2 import base64

3 import logging

4 import os

5 import traceback

6 from datetime import datetime

7 from os.path import basename

8 from typing import Optional, Sequence

9 import pytz

10 from django import forms

11 from django.conf import settings

12 from django.conf.urls import url

13 from django.contrib import admin

14 from django.contrib import messages

15 from django.contrib.admin import SimpleListFilter

16 from django.contrib.auth.models import User

17 from django.contrib.messages import add_message, ERROR

18 from django.core.exceptions import ValidationError

19 from django.core.files.uploadedfile import InMemoryUploadedFile

20 from django.db import transaction

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

22 from django.db.models.aggregates import Sum

23 from django.http import HttpRequest, Http404

24 from django.shortcuts import render, get_object_or_404

25 from django.urls import ResolverMatch, reverse

26 from django.utils.formats import date_format, localize

27 from django.utils.html import format_html

28 from django.utils.safestring import mark_safe

29 from django.utils.text import capfirst

30 from django.utils.translation import gettext_lazy as _

31 from jacc.admin import AccountEntryNoteInline, AccountEntryNoteAdmin

32 from jacc.models import Account, EntryType, AccountEntryNote

33 from jbank.x509_helpers import get_x509_cert_from_file

34 from jutil.request import get_ip

35 from jutil.responses import FormattedXmlResponse, FormattedXmlFileResponse

36 from jutil.xml import xml_to_dict

37 from jbank.helpers import create_statement, create_reference_payment_batch

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

57 from jbank.tito import parse_tiliote_statements_from_file, parse_tiliote_statements

58 from jbank.svm import parse_svm_batches_from_file, parse_svm_batches

59 from jutil.admin import ModelAdminBase, admin_log, admin_log_changed_fields

60

61 logger = logging.getLogger(__name__)

62

63

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1418

1419 admin.site.register(CurrencyExchangeSource, CurrencyExchangeSourceAdmin)

1420 admin.site.register(CurrencyExchange, CurrencyExchangeAdmin)

1421 admin.site.register(Payout, PayoutAdmin)

1422 admin.site.register(PayoutStatus, PayoutStatusAdmin)

1423 admin.site.register(PayoutParty, PayoutPartyAdmin)

1424 admin.site.register(Refund, RefundAdmin)

1425 admin.site.register(Statement, StatementAdmin)

1426 admin.site.register(StatementRecord, StatementRecordAdmin)

1427 admin.site.register(StatementFile, StatementFileAdmin)

1428 admin.site.register(ReferencePaymentRecord, ReferencePaymentRecordAdmin)

1429 admin.site.register(ReferencePaymentBatch, ReferencePaymentBatchAdmin)

1430 admin.site.register(ReferencePaymentBatchFile, ReferencePaymentBatchFileAdmin)

1431 admin.site.register(WsEdiConnection, WsEdiConnectionAdmin)

1432 admin.site.register(WsEdiSoapCall, WsEdiSoapCallAdmin)