Coverage for jbank/admin.py: 51%

# Coverage for jbank/admin.py : 51%

## 570 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

9

10 import pytz

11 from django import forms

12 from django.conf import settings

13 from django.conf.urls import url

14 from django.contrib import admin

15 from django.contrib import messages

16 from django.contrib.admin import SimpleListFilter

17 from django.contrib.auth.models import User

18 from django.contrib.messages import add_message, ERROR

19 from django.core.exceptions import ValidationError

20 from django.core.files.uploadedfile import InMemoryUploadedFile

21 from django.db import transaction

22 from django.db.models import F, Q

23 from django.db.models.aggregates import Sum

24 from django.http import HttpRequest, Http404

25 from django.shortcuts import render, get_object_or_404

26 from django.urls import ResolverMatch, reverse

27 from django.utils.formats import date_format

28 from django.utils.html import format_html

29 from django.utils.safestring import mark_safe

30 from django.utils.text import capfirst

31 from django.utils.translation import gettext_lazy as _

32 from jacc.models import Account, EntryType

33 from jbank.x509_helpers import get_x509_cert_from_file

34 from jutil.responses import FormattedXmlResponse, FormattedXmlFileResponse

35 from jutil.xml import xml_to_dict

36 from jbank.helpers import create_statement, create_reference_payment_batch

37 from jbank.models import Statement, StatementRecord, StatementRecordSepaInfo, ReferencePaymentRecord, \

38 ReferencePaymentBatch, StatementFile, ReferencePaymentBatchFile, Payout, Refund, PayoutStatus, PayoutParty, \

39 StatementRecordDetail, StatementRecordRemittanceInfo, CurrencyExchange, CurrencyExchangeSource, WsEdiConnection, \

40 WsEdiSoapCall

41 from jbank.parsers import parse_tiliote_statements, parse_tiliote_statements_from_file, parse_svm_batches_from_file, \

42 parse_svm_batches

43 from jutil.admin import ModelAdminBase, admin_log

44

45 logger = logging.getLogger(__name__)

46

47

48 class SettlementEntryTypesFilter(SimpleListFilter):

49 """

50 Filters incoming settlement type entries.

51 """

52 title = _('account entry types')

53 parameter_name = 'type'

54

55 def lookups(self, request, model_admin):

56 choices = []

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

58 assert isinstance(e, EntryType)

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

60 return choices

61

62 def queryset(self, request, queryset):

63 val = self.value()

64 if val:

65 return queryset.filter(type__id=val)

66 return queryset

67

68

69 class AccountEntryMatchedFilter(SimpleListFilter):

70 """

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

72 """

73 title = _('account.entry.matched.filter')

74 parameter_name = 'matched'

75

76 def lookups(self, request, model_admin):

77 return [

78 ('1', capfirst(_('account.entry.not.matched'))),

79 ('2', capfirst(_('account.entry.is.matched'))),

80 ('3', capfirst(_('marked as settled'))),

81 ]

82

83 def queryset(self, request, queryset):

84 val = self.value()

85 if val:

86 # return original settlements only

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

88 if val == '1':

89 # return those which are not manually settled and

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

91 queryset = queryset.exclude(manually_settled=True)

92 queryset = queryset.annotate(child_set_amount=Sum('child_set__amount'))

93 return queryset.filter(Q(child_set=None) | Q(child_set_amount__lt=F('amount')))

94 if val == '2':

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

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

97 if val == '3':

98 # return only manually marked as settled

99 return queryset.filter(manually_settled=True)

100 return queryset

101

102

103 class AccountNameFilter(SimpleListFilter):

104 """

105 Filters account entries based on account name.

106 """

107 title = _('account.name.filter')

108 parameter_name = 'account-name'

109

110 def lookups(self, request, model_admin):

111 ops = []

112 qs = model_admin.get_queryset(request)

113 for e in qs.distinct('account__name'):

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

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

116

117 def queryset(self, request, queryset):

118 val = self.value()

119 if val:

120 return queryset.filter(account__name=val)

121 return queryset

122

123

124 class StatementAdmin(ModelAdminBase):

125 exclude = ()

126 list_per_page = 20

127 save_on_top = False

128 ordering = ('-record_date', 'account_number')

129 date_hierarchy = 'record_date'

130 list_filter = (

131 'account_number',

132 )

133 readonly_fields = (

134 'file_link',

135 'account_number',

136 'statement_number',

137 'begin_date',

138 'end_date',

139 'record_date',

140 'customer_identifier',

141 'begin_balance_date',

142 'begin_balance',

143 'record_count',

144 'currency_code',

145 'account_name',

146 'account_limit',

147 'owner_name',

148 'contact_info_1',

149 'contact_info_2',

150 'bank_specific_info_1',

151 'iban',

152 'bic',

153 )

154 fields = readonly_fields

155 search_fields = (

156 'name',

157 'statement_number',

158 )

159 list_display = (

160 'id',

161 'record_date',

162 'account_number',

163 'statement_number',

164 'begin_balance',

165 'currency_code',

166 'file_link',

167 'account_entry_list'

168 )

169

170 def account_entry_list(self, obj):

171 assert isinstance(obj, Statement)

172 admin_url = reverse('admin:jbank_statementrecord_statement_changelist', args=(obj.id, ))

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

174 account_entry_list.short_description = _('account entries') # type: ignore

175

176 def file_link(self, obj):

177 assert isinstance(obj, Statement)

178 if not obj.file:

179 return ''

180 admin_url = reverse('admin:jbank_statementfile_change', args=(obj.file.id, ))

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

182 file_link.admin_order_field = 'file' # type: ignore

183 file_link.short_description = _('file') # type: ignore

184

185 def get_urls(self):

186 return [

187 url(r'^by-file/(?P<file_id>\d+)/$', self.admin_site.admin_view(self.kw_changelist_view), name='jbank_statement_file_changelist'),

188 ] + super().get_urls()

189

190 def get_queryset(self, request: HttpRequest):

191 rm = request.resolver_match

192 assert isinstance(rm, ResolverMatch)

193 qs = super().get_queryset(request)

194 file_id = rm.kwargs.get('file_id', None)

195 if file_id:

196 qs = qs.filter(file=file_id)

197 return qs

198

199

200 class StatementRecordDetailInlineAdmin(admin.StackedInline):

201 exclude = ()

202 model = StatementRecordDetail

203 can_delete = False

204 extra = 0

205

206 fields = (

207 'batch_identifier',

208 'amount',

209 'currency_code',

210 'instructed_amount',

211 'exchange',

212 'archive_identifier',

213 'end_to_end_identifier',

214 'creditor_name',

215 'debtor_name',

216 'ultimate_debtor_name',

217 'unstructured_remittance_info',

218 'paid_date',

219 'structured_remittance_info',

220 )

221 readonly_fields = fields

222 raw_id_fields = (

223 )

224

225 def structured_remittance_info(self, obj):

226 assert isinstance(obj, StatementRecordDetail)

227 lines = []

228 for rinfo in obj.remittanceinfo_set.all().order_by('id'):

229 assert isinstance(rinfo, StatementRecordRemittanceInfo)

230 lines.append(str(rinfo))

231 return mark_safe('<br>'.join(lines))

232 structured_remittance_info.short_description = _('structured remittance info') # type: ignore

233

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

235 return False

236

237

238 class StatementRecordSepaInfoInlineAdmin(admin.StackedInline):

239 exclude = ()

240 model = StatementRecordSepaInfo

241 can_delete = False

242 extra = 0

243 max_num = 1

244

245 readonly_fields = (

246 'record',

247 'reference',

248 'iban_account_number',

249 'bic_code',

250 'recipient_name_detail',

251 'payer_name_detail',

252 'identifier',

253 'archive_identifier',

254 )

255 raw_id_fields = (

256 'record',

257 )

258

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

260 return False

261

262

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

264 try:

265 data = request.POST.dict()

266

267 if 'description' in data:

268 description = data['description']

269 user = request.user

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

271 e.manually_settled = True

272 e.save(update_fields=['manually_settled'])

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

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

275 messages.info(request, msg)

276 else:

277 cx = {

278 'qs': qs,

279 }

280 return render(request, 'admin/jbank/mark_as_manually_settled.html', context=cx)

281 except ValidationError as e:

282 messages.error(request, ' '.join(e.messages))

283 except Exception as e:

284 logger.error('mark_as_manually_settled: %s', traceback.format_exc())

285 messages.error(request, '{}'.format(e))

286 return None

287

288

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

290 user = request.user

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

292 e.manually_settled = False

293 e.save(update_fields=['manually_settled'])

294 msg = capfirst(_('manually settled flag cleared'))

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

296 messages.info(request, msg)

297

298

299 class StatementRecordAdmin(ModelAdminBase):

300 exclude = ()

301 list_per_page = 25

302 save_on_top = False

303 date_hierarchy = 'record_date'

304 readonly_fields = (

305 'id',

306 'entry_type',

307 'statement',

308 'line_number',

309 'file_link',

310 'record_number',

311 'archive_identifier',

312 'record_date',

313 'value_date',

314 'paid_date',

315 'type',

316 'record_code',

317 'record_domain',

318 'family_code',

319 'sub_family_code',

320 'record_description',

321 'amount',

322 'receipt_code',

323 'delivery_method',

324 'name',

325 'name_source',

326 'recipient_account_number',

327 'recipient_account_number_changed',

328 'remittance_info',

329 'messages',

330 'client_messages',

331 'bank_messages',

332 'archived',

333 'manually_settled',

334 # from AccountEntry

335 'account',

336 'timestamp',

337 'created',

338 'last_modified',

339 'type',

340 'description',

341 'amount',

342 'source_file',

343 'source_invoice',

344 'settled_invoice',

345 'settled_item',

346 'parent',

347 )

348 raw_id_fields = (

349 'statement',

350 # from AccountEntry

351 'account',

352 'source_file',

353 'parent',

354 'source_invoice',

355 'settled_invoice',

356 'settled_item',

357 )

358 list_filter = (

359 'statement__file__tag',

360 AccountNameFilter,

361 AccountEntryMatchedFilter,

362 SettlementEntryTypesFilter,

363 'record_code',

364 )

365 search_fields = (

366 '=archive_identifier',

367 '=amount',

368 '=recipient_account_number',

369 'record_description',

370 'name',

371 'remittance_info',

372 'messages',

373 )

374 list_display = (

375 'id',

376 'record_date',

377 'type',

378 'record_code',

379 'amount',

380 'name',

381 'recipient_account_number',

382 'remittance_info',

383 'source_file_link'

384 )

385 inlines = (

386 StatementRecordSepaInfoInlineAdmin,

387 StatementRecordDetailInlineAdmin,

388 )

389 actions = (

390 mark_as_manually_settled,

391 unmark_manually_settled_flag,

392 )

393

394 def get_urls(self):

395 return [

396 url(r'^by-statement/(?P<statement_id>\d+)/$', self.admin_site.admin_view(self.kw_changelist_view),

397 name='jbank_statementrecord_statement_changelist'),

398 ] + super().get_urls()

399

400 def get_queryset(self, request: HttpRequest):

401 rm = request.resolver_match

402 assert isinstance(rm, ResolverMatch)

403 qs = super().get_queryset(request)

404 statement_id = rm.kwargs.get('statement_id', None)

405 if statement_id:

406 qs = qs.filter(statement__id=statement_id)

407 return qs

408

409 def source_file_link(self, obj):

410 assert isinstance(obj, StatementRecord)

411 if not obj.statement:

412 return ''

413 admin_url = reverse('admin:jbank_statementfile_change', args=(obj.statement.file.id, ))

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

415 source_file_link.admin_order_field = 'statement' # type: ignore

416 source_file_link.short_description = _('source file') # type: ignore

417

418 def file_link(self, obj):

419 assert isinstance(obj, StatementRecord)

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

421 return ''

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

423 admin_url = reverse('admin:jbank_statementfile_change', args=(obj.statement.file.id, ))

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

425 file_link.admin_order_field = 'file' # type: ignore

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

427

428

429 class ReferencePaymentRecordAdmin(ModelAdminBase):

430 exclude = ()

431 list_per_page = 25

432 save_on_top = False

433 date_hierarchy = 'record_date'

434 raw_id_fields = (

435 'batch',

436 # from AccountEntry

437 'account',

438 'source_file',

439 'parent',

440 'source_invoice',

441 'settled_invoice',

442 'settled_item',

443 )

444 readonly_fields = (

445 'id',

446 'batch',

447 'line_number',

448 'file_link',

449 'record_type',

450 'account_number',

451 'record_date',

452 'paid_date',

453 'archive_identifier',

454 'remittance_info',

455 'payer_name',

456 'currency_identifier',

457 'name_source',

458 'amount',

459 'correction_identifier',

460 'delivery_method',

461 'receipt_code',

462 'archived',

463 'manually_settled',

464 # from AccountEntry

465 'account',

466 'timestamp',

467 'created',

468 'last_modified',

469 'type',

470 'description',

471 'amount',

472 'source_file',

473 'source_invoice',

474 'settled_invoice',

475 'settled_item',

476 'parent',

477 )

478 list_filter = (

479 'batch__file__tag',

480 AccountNameFilter,

481 AccountEntryMatchedFilter,

482 'correction_identifier',

483 )

484 search_fields = (

485 '=archive_identifier',

486 '=amount',

487 'remittance_info',

488 'payer_name',

489 'batch__name',

490 )

491 list_display = (

492 'id',

493 'record_date',

494 'type',

495 'amount',

496 'payer_name',

497 'remittance_info',

498 'source_file_link',

499 )

500 actions = (

501 mark_as_manually_settled,

502 unmark_manually_settled_flag,

503 )

504

505 def file_link(self, obj):

506 assert isinstance(obj, ReferencePaymentRecord)

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

508 return ''

509 admin_url = reverse('admin:jbank_referencepaymentbatchfile_change', args=(obj.batch.file.id, ))

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

511 file_link.admin_order_field = 'file' # type: ignore

512 file_link.short_description = _('file') # type: ignore

513

514 def get_urls(self):

515 return [

516 url(r'^by-batch/(?P<batch_id>\d+)/$', self.admin_site.admin_view(self.kw_changelist_view),

517 name='jbank_referencepaymentrecord_batch_changelist'),

518 ] + super().get_urls()

519

520 def get_queryset(self, request: HttpRequest):

521 rm = request.resolver_match

522 assert isinstance(rm, ResolverMatch)

523 qs = super().get_queryset(request)

524 batch_id = rm.kwargs.get('batch_id', None)

525 if batch_id:

526 qs = qs.filter(batch_id=batch_id)

527 return qs

528

529 def source_file_link(self, obj):

530 assert isinstance(obj, ReferencePaymentRecord)

531 if not obj.batch:

532 return ''

533 admin_url = reverse('admin:jbank_referencepaymentbatchfile_change', args=(obj.batch.file.id, ))

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

535 source_file_link.admin_order_field = 'batch' # type: ignore

536 source_file_link.short_description = _('account entry source file') # type: ignore

537

538

539 class ReferencePaymentBatchAdmin(ModelAdminBase):

540 exclude = ()

541 list_per_page = 20

542 save_on_top = False

543 ordering = ('-record_date',)

544 date_hierarchy = 'record_date'

545 list_filter = (

546 'record_set__account_number',

547 )

548 fields = (

549 'file_link',

550 'record_date',

551 'institution_identifier',

552 'service_identifier',

553 'currency_identifier',

554 )

555 readonly_fields = (

556 'name',

557 'file',

558 'file_link',

559 'record_date',

560 'institution_identifier',

561 'service_identifier',

562 'currency_identifier',

563 )

564 search_fields = (

565 'name',

566 '=record_set__archive_identifier',

567 '=record_set__amount',

568 'record_set__remittance_info',

569 'record_set__payer_name',

570 )

571 list_display = (

572 'id',

573 'name',

574 'record_date',

575 'service_identifier',

576 'currency_identifier',

577 'account_entry_list',

578 )

579

580 def account_entry_list(self, obj):

581 assert isinstance(obj, ReferencePaymentBatch)

582 admin_url = reverse('admin:jbank_referencepaymentrecord_batch_changelist', args=(obj.id, ))

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

584 account_entry_list.short_description = _('account entries') # type: ignore

585

586 def file_link(self, obj):

587 assert isinstance(obj, ReferencePaymentBatch)

588 if not obj.file:

589 return ''

590 admin_url = reverse('admin:jbank_referencepaymentbatchfile_change', args=(obj.file.id, ))

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

592 file_link.admin_order_field = 'file' # type: ignore

593 file_link.short_description = _('file') # type: ignore

594

595 def get_urls(self):

596 return [

597 url(r'^by-file/(?P<file_id>\d+)/$', self.admin_site.admin_view(self.kw_changelist_view), name='jbank_referencepaymentbatch_file_changelist'),

598 ] + super().get_urls()

599

600 def get_queryset(self, request: HttpRequest):

601 rm = request.resolver_match

602 assert isinstance(rm, ResolverMatch)

603 qs = super().get_queryset(request)

604 file_id = rm.kwargs.get('file_id', None)

605 if file_id:

606 qs = qs.filter(file=file_id)

607 return qs

608

609

610 class StatementFileForm(forms.ModelForm):

611 class Meta:

612 fields = [

613 'file',

614 ]

615

616 def clean_file(self):

617 file = self.cleaned_data['file']

618 assert isinstance(file, InMemoryUploadedFile)

619 name = file.name

620 file.seek(0)

621 content = file.read()

622 assert isinstance(content, bytes)

623 try:

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

625 for stm in statements:

626 account_number = stm['header']['account_number']

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

628 raise ValidationError(_('account.not.found').format(account_number=account_number))

629 except ValidationError:

630 raise

631 except Exception as e:

632 raise ValidationError(_('Unhandled error') + ': {}'.format(e))

633 return file

634

635

636 class StatementFileAdmin(ModelAdminBase):

637 save_on_top = False

638 exclude = ()

639 form = StatementFileForm

640

641 date_hierarchy = 'created'

642

643 search_fields = (

644 'original_filename__contains',

645 )

646

647 list_filter = (

648 'tag',

649 )

650

651 list_display = (

652 'id',

653 'created',

654 'file',

655 )

656

657 readonly_fields = (

658 'created',

659 'errors',

660 'file',

661 'original_filename',

662 )

663

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

665 return False

666

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

668 if add:

669 instance = form.instance

670 assert isinstance(instance, StatementFile)

671 if instance.file:

672 full_path = instance.full_path

673 plain_filename = basename(full_path)

674 try:

675 statements = parse_tiliote_statements_from_file(full_path)

676 with transaction.atomic():

677 for data in statements:

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

679 except Exception as e:

680 instance.errors = traceback.format_exc()

681 instance.save()

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

683 instance.delete()

684

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

686

687

688 class ReferencePaymentBatchFileForm(forms.ModelForm):

689 class Meta:

690 fields = [

691 'file',

692 ]

693

694 def clean_file(self):

695 file = self.cleaned_data['file']

696 assert isinstance(file, InMemoryUploadedFile)

697 name = file.name

698 file.seek(0)

699 content = file.read()

700 assert isinstance(content, bytes)

701 try:

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

703 for b in batches:

704 for rec in b['records']:

705 account_number = rec['account_number']

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

707 raise ValidationError(_('account.not.found').format(account_number=account_number))

708 except ValidationError:

709 raise

710 except Exception as e:

711 raise ValidationError(_('Unhandled error') + ': {}'.format(e))

712 return file

713

714

715 class ReferencePaymentBatchFileAdmin(ModelAdminBase):

716 save_on_top = False

717 exclude = ()

718 form = ReferencePaymentBatchFileForm

719 date_hierarchy = 'created'

720

721 list_display = (

722 'id',

723 'created',

724 'file',

725 )

726

727 list_filter = (

728 'tag',

729 )

730

731 search_fields = (

732 'file__contains',

733 )

734

735 readonly_fields = (

736 'created',

737 'errors',

738 'file',

739 'original_filename',

740 )

741

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

743 return False

744

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

746 if add:

747 instance = form.instance

748 assert isinstance(instance, ReferencePaymentBatchFile)

749 if instance.file:

750 full_path = instance.full_path

751 plain_filename = basename(full_path)

752 try:

753 batches = parse_svm_batches_from_file(full_path)

754 with transaction.atomic():

755 for data in batches:

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

757 except Exception as e:

758 user = request.user

759 assert isinstance(user, User)

760 instance.errors = traceback.format_exc()

761 instance.save()

762 msg = str(e)

763 if user.is_superuser:

764 msg = instance.errors

765 logger.error('%s: %s', plain_filename, msg)

766 add_message(request, ERROR, msg)

767 instance.delete()

768

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

770

771

772 class PayoutStatusAdmin(ModelAdminBase):

773 fields = (

774 'created',

775 'payout',

776 'file_name_link',

777 'response_code',

778 'response_text',

779 'msg_id',

780 'original_msg_id',

781 'group_status',

782 'status_reason',

783 )

784 readonly_fields = fields

785 list_display = (

786 'id',

787 'created',

788 'payout',

789 'file_name_link',

790 'response_code',

791 'response_text',

792 'original_msg_id',

793 'group_status',

794 )

795

796 def file_download_view(self, request, pk, filename, form_url='', extra_context=None): # pylint: disable=unused-argument

797 user = request.user

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

799 raise Http404(_('File {} not found').format(filename))

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

801 assert isinstance(obj, PayoutStatus)

802 full_path = obj.full_path

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

804 raise Http404(_('File {} not found').format(filename))

805 return FormattedXmlFileResponse(full_path)

806

807 def file_name_link(self, obj):

808 assert isinstance(obj, PayoutStatus)

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

810 return obj.file_name

811 admin_url = reverse('admin:jbank_payoutstatus_file_download', args=(obj.id, obj.file_name,))

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

813 file_name_link.short_description = _('file') # type: ignore

814 file_name_link.admin_order_field = 'file_name' # type: ignore

815

816 def get_urls(self):

817 urls = [

818 url(r'^(\d+)/change/status-downloads/(.+)/$', self.file_download_view, name='jbank_payoutstatus_file_download'),

819 ]

820 return urls + super().get_urls()

821

822

823 class PayoutStatusInlineAdmin(admin.TabularInline):

824 model = PayoutStatus

825 can_delete = False

826 extra = 0

827 ordering = ('-id', )

828 fields = PayoutStatusAdmin.fields

829 readonly_fields = PayoutStatusAdmin.readonly_fields

830

831 def file_name_link(self, obj):

832 assert isinstance(obj, PayoutStatus)

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

834 return obj.file_name

835 admin_url = reverse('admin:jbank_payoutstatus_file_download', args=(obj.id, obj.file_name,))

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

837 file_name_link.short_description = _('file') # type: ignore

838 file_name_link.admin_order_field = 'file_name' # type: ignore

839

840

841 class PayoutAdmin(ModelAdminBase):

842 save_on_top = False

843 exclude = ()

844 inlines = [PayoutStatusInlineAdmin]

845 date_hierarchy = 'timestamp'

846

847 raw_id_fields = (

848 'account',

849 'parent',

850 'payer',

851 'recipient',

852 )

853

854 list_filter = (

855 'state',

856 'payoutstatus_set__response_code',

857 'payoutstatus_set__group_status',

858 'recipient__bic',

859 )

860

861 fields = (

862 'connection',

863 'account',

864 'parent',

865 'payer',

866 'recipient',

867 'amount',

868 'messages',

869 'reference',

870 'due_date',

871 'msg_id',

872 'file_name',

873 'timestamp',

874 'paid_date',

875 'state',

876 'group_status',

877 'created',

878 )

879

880 list_display = (

881 'id',

882 'timestamp',

883 'recipient',

884 'amount',

885 'paid_date',

886 'state',

887 )

888

889 readonly_fields = (

890 'created',

891 'paid_date',

892 'timestamp',

893 'msg_id',

894 'file_name',

895 'group_status',

896 )

897

898 search_fields = (

899 '=msg_id',

900 '=file_name',

901 '=file_reference',

902 'recipient__name',

903 '=recipient__account_number',

904 '=msg_id',

905 '=amount',

906 )

907

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

909 assert isinstance(obj, Payout)

910 if not change:

911 if not hasattr(obj, 'account') or not obj.account:

912 obj.account = obj.payer.payouts_account

913 if not hasattr(obj, 'type') or not obj.type:

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

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

916

917

918 class PayoutPartyAdmin(ModelAdminBase):

919 save_on_top = False

920 exclude = ()

921 search_fields = (

922 'name',

923 '=account_number',

924 '=org_id',

925 )

926 ordering = ('name',)

927

928 actions = (

929 )

930

931 list_display = (

932 'id',

933 'name',

934 'account_number',

935 'bic',

936 'org_id',

937 'address',

938 'country_code',

939 )

940

941 raw_id_fields = (

942 'payouts_account',

943 )

944

945

946 class RefundAdmin(PayoutAdmin):

947 raw_id_fields = (

948 'account',

949 'parent',

950 'payer',

951 'recipient',

952 )

953

954 fields = (

955 'connection',

956 'account',

957 'payer',

958 'parent',

959 'recipient',

960 'amount',

961 'messages',

962 'reference',

963 'due_date',

964 'msg_id',

965 'file_name',

966 'timestamp',

967 'paid_date',

968 'state',

969 'group_status',

970 'created',

971 )

972

973

974 class CurrencyExchangeSourceAdmin(ModelAdminBase):

975 save_on_top = False

976 exclude = ()

977

978 fields = (

979 'id',

980 'created',

981 'name',

982 )

983

984 readonly_fields = (

985 'id',

986 'created',

987 )

988

989 list_display = fields

990

991

992 class CurrencyExchangeAdmin(ModelAdminBase):

993 save_on_top = False

994

995 fields = (

996 'record_date',

997 'source_currency',

998 'target_currency',

999 'unit_currency',

1000 'exchange_rate',

1001 'source',

1002 )

1003

1004 date_hierarchy = 'record_date'

1005 readonly_fields = list_display = fields

1006 raw_id_fields = ('source', )

1007 list_filter = ('source_currency', 'target_currency', 'source')

1008

1009

1010 class WsEdiConnectionAdmin(ModelAdminBase):

1011 save_on_top = False

1012

1013 list_display = (

1014 'id',

1015 'created',

1016 'name',

1017 'sender_identifier',

1018 'receiver_identifier',

1019 'expires',

1020 )

1021

1022 raw_id_fields = (

1023 )

1024

1025 fieldsets = (

1026 (None, {

1027 'fields': [

1028 'id',

1029 'name',

1030 'enabled',

1031 'sender_identifier',

1032 'receiver_identifier',

1033 'target_identifier',

1034 'environment',

1035 'debug_commands',

1036 'created',

1037 ]

1038 }),

1039 ('PKI', {

1040 'fields': [

1041 'pki_endpoint',

1042 'pin',

1043 'bank_root_cert_file',

1044 ]

1045 }),

1046 ('EDI', {

1047 'fields': [

1048 'soap_endpoint',

1049 'signing_cert_file',

1050 'signing_key_file',

1051 'encryption_cert_file',

1052 'encryption_key_file',

1053 'bank_encryption_cert_file',

1054 'bank_signing_cert_file',

1055 'ca_cert_file',

1056 ]

1057 }),

1058 )

1059

1060 readonly_fields = (

1061 'id',

1062 'created',

1063 'expires',

1064 )

1065

1066 def expires(self, obj):

1067 assert isinstance(obj, WsEdiConnection)

1068 min_not_valid_after: Optional[datetime] = None

1069 try:

1070 certs = [

1071 obj.signing_cert_full_path,

1072 obj.encryption_cert_full_path,

1073 obj.bank_encryption_cert_full_path,

1074 obj.bank_root_cert_full_path,

1075 obj.ca_cert_full_path,

1076 ]

1077 except Exception as e:

1078 logger.error(e)

1079 return _('(missing certificate files)')

1080 for filename in certs:

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

1082 cert = get_x509_cert_from_file(filename)

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

1084 if min_not_valid_after is None or not_valid_after < min_not_valid_after:

1085 min_not_valid_after = not_valid_after

1086 return date_format(min_not_valid_after.date(), 'SHORT_DATE_FORMAT') if min_not_valid_after else ''

1087 expires.short_description = _('expires') # type: ignore

1088

1089

1090 class WsEdiSoapCallAdmin(ModelAdminBase):

1091 save_on_top = False

1092

1093 date_hierarchy = 'created'

1094

1095 list_display = (

1096 'id',

1097 'created',

1098 'connection',

1099 'command',

1100 'executed',

1101 'execution_time',

1102 )

1103

1104 list_filter = (

1105 'connection',

1106 'command',

1107 )

1108

1109 raw_id_fields = (

1110 )

1111

1112 fields = (

1113 'id',

1114 'connection',

1115 'command',

1116 'created',

1117 'executed',

1118 'execution_time',

1119 'error_fmt',

1120 'admin_application_request',

1121 'admin_application_response',

1122 'admin_application_response_file',

1123 )

1124

1125 readonly_fields = (

1126 'id',

1127 'connection',

1128 'command',

1129 'created',

1130 'executed',

1131 'execution_time',

1132 'error_fmt',

1133 'admin_application_request',

1134 'admin_application_response',

1135 'admin_application_response_file',

1136 )

1137

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

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

1140 if not request.user.is_superuser:

1141 fields = fields[:-2]

1142 return fields

1143

1144 def soap_download_view(self, request, object_id, file_type, form_url = '', extra_context = None): # pylint: disable=unused-argument

1145 user = request.user

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

1147 raise Http404('File not found')

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

1149 assert isinstance(obj, WsEdiSoapCall)

1150 if file_type == 'f':

1151 with open(obj.debug_response_full_path, 'rb') as fb:

1152 data = xml_to_dict(fb.read())

1153 content = base64.b64decode(data.get('Content', ''))

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

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

1156

1157 def admin_application_request(self, obj):

1158 assert isinstance(obj, WsEdiSoapCall)

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

1160 return ''

1161 download_url = reverse('admin:jbank_wsedisoapcall_soap_download', args=[str(obj.id), 'a'])

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

1163 admin_application_request.short_description = _('application request') # type: ignore

1164

1165 def admin_application_response(self, obj):

1166 assert isinstance(obj, WsEdiSoapCall)

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

1168 return ''

1169 download_url = reverse('admin:jbank_wsedisoapcall_soap_download', args=[str(obj.id), 'r'])

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

1171 admin_application_response.short_description = _('application response') # type: ignore

1172

1173 def admin_application_response_file(self, obj):

1174 assert isinstance(obj, WsEdiSoapCall)

1175 if obj.command != 'DownloadFile' or not obj.executed:

1176 return ''

1177 file_type = 'f'

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

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

1180 admin_application_response_file.short_description = _('file') # type: ignore

1181

1182 def execution_time(self, obj):

1183 assert isinstance(obj, WsEdiSoapCall)

1184 return obj.executed - obj.created if obj.executed else ''

1185 execution_time.short_description = _('execution time') # type: ignore

1186

1187 def error_fmt(self, obj):

1188 assert isinstance(obj, WsEdiSoapCall)

1189 return mark_safe(obj.error.replace('\n', '<br>'))

1190 error_fmt.short_description = _('error') # type: ignore

1191

1192 def get_urls(self):

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

1194 return [

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

1196 ] + super().get_urls()

1197

1198

1199 mark_as_manually_settled.short_description = _('Mark as manually settled') # type: ignore

1200 unmark_manually_settled_flag.short_description = _('Unmark manually settled flag') # type: ignore

1201

1202 admin.site.register(CurrencyExchangeSource, CurrencyExchangeSourceAdmin)

1203 admin.site.register(CurrencyExchange, CurrencyExchangeAdmin)

1204 admin.site.register(Payout, PayoutAdmin)

1205 admin.site.register(PayoutStatus, PayoutStatusAdmin)

1206 admin.site.register(PayoutParty, PayoutPartyAdmin)

1207 admin.site.register(Refund, RefundAdmin)

1208 admin.site.register(Statement, StatementAdmin)

1209 admin.site.register(StatementRecord, StatementRecordAdmin)

1210 admin.site.register(StatementFile, StatementFileAdmin)

1211 admin.site.register(ReferencePaymentRecord, ReferencePaymentRecordAdmin)

1212 admin.site.register(ReferencePaymentBatch, ReferencePaymentBatchAdmin)

1213 admin.site.register(ReferencePaymentBatchFile, ReferencePaymentBatchFileAdmin)

1214 admin.site.register(WsEdiConnection, WsEdiConnectionAdmin)

1215 admin.site.register(WsEdiSoapCall, WsEdiSoapCallAdmin)