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 

9 

10import pytz 

11from django import forms 

12from django.conf import settings 

13from django.conf.urls import url 

14from django.contrib import admin 

15from django.contrib import messages 

16from django.contrib.admin import SimpleListFilter 

17from django.contrib.auth.models import User 

18from django.contrib.messages import add_message, ERROR 

19from django.core.exceptions import ValidationError 

20from django.core.files.uploadedfile import InMemoryUploadedFile 

21from django.db import transaction 

22from django.db.models import F, Q 

23from django.db.models.aggregates import Sum 

24from django.http import HttpRequest, Http404 

25from django.shortcuts import render, get_object_or_404 

26from django.urls import ResolverMatch, reverse 

27from django.utils.formats import date_format 

28from django.utils.html import format_html 

29from django.utils.safestring import mark_safe 

30from django.utils.text import capfirst 

31from django.utils.translation import gettext_lazy as _ 

32from jacc.models import Account, EntryType 

33from jbank.x509_helpers import get_x509_cert_from_file 

34from jutil.responses import FormattedXmlResponse, FormattedXmlFileResponse 

35from jutil.xml import xml_to_dict 

36from jbank.helpers import create_statement, create_reference_payment_batch 

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

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

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

40 WsEdiSoapCall 

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

42 parse_svm_batches 

43from jutil.admin import ModelAdminBase, admin_log 

44 

45logger = logging.getLogger(__name__) 

46 

47 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1201 

1202admin.site.register(CurrencyExchangeSource, CurrencyExchangeSourceAdmin) 

1203admin.site.register(CurrencyExchange, CurrencyExchangeAdmin) 

1204admin.site.register(Payout, PayoutAdmin) 

1205admin.site.register(PayoutStatus, PayoutStatusAdmin) 

1206admin.site.register(PayoutParty, PayoutPartyAdmin) 

1207admin.site.register(Refund, RefundAdmin) 

1208admin.site.register(Statement, StatementAdmin) 

1209admin.site.register(StatementRecord, StatementRecordAdmin) 

1210admin.site.register(StatementFile, StatementFileAdmin) 

1211admin.site.register(ReferencePaymentRecord, ReferencePaymentRecordAdmin) 

1212admin.site.register(ReferencePaymentBatch, ReferencePaymentBatchAdmin) 

1213admin.site.register(ReferencePaymentBatchFile, ReferencePaymentBatchFileAdmin) 

1214admin.site.register(WsEdiConnection, WsEdiConnectionAdmin) 

1215admin.site.register(WsEdiSoapCall, WsEdiSoapCallAdmin)