Coverage for website/payments/admin.py: 100.00%

339 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2025-08-14 10:31 +0000

1import csv 

2from collections import OrderedDict 

3 

4from django.contrib import admin, messages 

5from django.contrib.admin import ModelAdmin 

6from django.contrib.admin.options import IncorrectLookupParameters 

7from django.contrib.admin.utils import model_ngettext 

8from django.db.models import QuerySet 

9from django.db.models.query_utils import Q 

10from django.http import HttpRequest, HttpResponse 

11from django.urls import path, reverse 

12from django.utils import timezone 

13from django.utils.html import format_html 

14from django.utils.text import capfirst 

15from django.utils.translation import gettext_lazy as _ 

16 

17from payments import admin_views, payables, services 

18from payments.forms import BankAccountAdminForm, BatchPaymentInlineAdminForm 

19 

20from .models import BankAccount, Batch, Payment, PaymentUser 

21 

22 

23def _show_message( 

24 model_admin: ModelAdmin, request: HttpRequest, n: int, message: str, error: str 

25) -> None: 

26 if n == 0: 

27 model_admin.message_user(request, error, messages.ERROR) 

28 else: 

29 model_admin.message_user( 

30 request, 

31 message % {"count": n, "items": model_ngettext(model_admin.opts, n)}, 

32 messages.SUCCESS, 

33 ) 

34 

35 

36class PayableModelListFilter(admin.SimpleListFilter): 

37 title = _("payable model") 

38 parameter_name = "payable_model" 

39 

40 def lookups(self, request, model_admin): 

41 return [ 

42 ( 

43 model._meta.get_field("payment").related_query_name(), 

44 f"{model._meta.app_label} | {model._meta.verbose_name}", 

45 ) 

46 for model in payables.payables.get_payable_models() 

47 ] + [("none", _("None"))] 

48 

49 def queryset(self, request, queryset): 

50 value = self.value() 

51 if not value: 

52 return queryset 

53 if value == "none": 

54 return queryset.filter( 

55 **{ 

56 model._meta.get_field("payment").related_query_name(): None 

57 for model in payables.payables.get_payable_models() 

58 } 

59 ) 

60 if value not in ( 

61 model._meta.get_field("payment").related_query_name() 

62 for model in payables.payables.get_payable_models() 

63 ): 

64 raise IncorrectLookupParameters(_("Invalid payable model")) 

65 return queryset.filter(**{value + "__isnull": False}) 

66 

67 

68@admin.register(Payment) 

69class PaymentAdmin(admin.ModelAdmin): 

70 """Manage the payments.""" 

71 

72 list_display = ( 

73 "created_at", 

74 "amount", 

75 "type", 

76 "paid_by_link", 

77 "processed_by_link", 

78 "batch_link", 

79 "topic", 

80 ) 

81 list_filter = ("type", "batch", PayableModelListFilter) 

82 list_select_related = ("paid_by", "processed_by", "batch") 

83 date_hierarchy = "created_at" 

84 fields = ( 

85 "created_at", 

86 "amount", 

87 "type", 

88 "paid_by", 

89 "processed_by", 

90 "topic", 

91 "notes", 

92 "batch", 

93 "payable_object", 

94 ) 

95 

96 search_fields = ( 

97 "topic", 

98 "notes", 

99 "paid_by__username", 

100 "paid_by__first_name", 

101 "paid_by__last_name", 

102 "processed_by__username", 

103 "processed_by__first_name", 

104 "processed_by__last_name", 

105 "amount", 

106 ) 

107 ordering = ("-created_at",) 

108 autocomplete_fields = ("paid_by", "processed_by") 

109 actions = [ 

110 "add_to_new_batch", 

111 "add_to_last_batch", 

112 "export_csv", 

113 ] 

114 

115 @admin.display(description=_("payable object")) 

116 def payable_object(self, obj: Payment) -> str: 

117 payable_object = obj.payable_object 

118 if payable_object: 

119 return format_html( 

120 "<a href='{}'>{}</a>", 

121 reverse( 

122 f"admin:{payable_object._meta.app_label}_{payable_object._meta.model_name}_change", 

123 args=[payable_object.pk], 

124 ), 

125 payable_object, 

126 ) 

127 return "None" 

128 

129 @staticmethod 

130 def _member_link(member: PaymentUser) -> str: 

131 return ( 

132 format_html( 

133 "<a href='{}'>{}</a>", member.get_absolute_url(), member.get_full_name() 

134 ) 

135 if member 

136 else None 

137 ) 

138 

139 def paid_by_link(self, obj: Payment) -> str: 

140 return self._member_link(obj.paid_by) 

141 

142 paid_by_link.admin_order_field = "paid_by" 

143 paid_by_link.short_description = _("paid by") 

144 

145 @staticmethod 

146 def _batch_link(payment: Payment, batch: Batch) -> str: 

147 if batch: 

148 return format_html( 

149 "<a href='{}'>{}</a>", batch.get_absolute_url(), str(batch) 

150 ) 

151 if payment.type == Payment.TPAY: 

152 return _("No batch attached") 

153 return "" 

154 

155 def batch_link(self, obj: Payment) -> str: 

156 return self._batch_link(obj, obj.batch) 

157 

158 batch_link.admin_order_field = "batch" 

159 batch_link.short_description = _("in batch") 

160 

161 def processed_by_link(self, obj: Payment) -> str: 

162 return self._member_link(obj.processed_by) 

163 

164 processed_by_link.admin_order_field = "processed_by" 

165 processed_by_link.short_description = _("processed by") 

166 

167 def has_delete_permission(self, request, obj=None): 

168 if isinstance(obj, Payment): 

169 if obj.batch and obj.batch.processed: 

170 return False 

171 if ( 

172 "payment/" in request.path 

173 and request.POST 

174 and request.POST.get("action") == "delete_selected" 

175 ): 

176 for payment_id in request.POST.getlist("_selected_action"): 

177 payment = Payment.objects.get(id=payment_id) 

178 if payment.batch and payment.batch.processed: 

179 return False 

180 

181 return super().has_delete_permission(request, obj) 

182 

183 def get_field_queryset(self, db, db_field, request): 

184 if str(db_field) == "payments.Payment.batch": 

185 return Batch.objects.filter(processed=False) 

186 return super().get_field_queryset(db, db_field, request) 

187 

188 def get_readonly_fields(self, request: HttpRequest, obj: Payment = None): 

189 readonly_fields = "created_at", "processed_by", "payable_object" 

190 if not obj: 

191 return readonly_fields + ("batch",) 

192 if obj.type == Payment.TPAY and not (obj.batch and obj.batch.processed): 

193 return readonly_fields + ( 

194 "amount", 

195 "paid_by", 

196 "type", 

197 "topic", 

198 "notes", 

199 ) 

200 return readonly_fields + ( 

201 "amount", 

202 "paid_by", 

203 "type", 

204 "topic", 

205 "notes", 

206 "batch", 

207 ) 

208 

209 def get_actions(self, request: HttpRequest) -> OrderedDict: 

210 """Get the actions for the payments. 

211 

212 Hide the processing actions if the right permissions are missing 

213 """ 

214 actions = super().get_actions(request) 

215 if not request.user.has_perm("payments.process_batches"): 

216 del actions["add_to_new_batch"] 

217 del actions["add_to_last_batch"] 

218 

219 return actions 

220 

221 def add_to_new_batch(self, request: HttpRequest, queryset: QuerySet) -> None: 

222 """Add selected TPAY payments to a new batch.""" 

223 tpays = queryset.filter(type=Payment.TPAY) 

224 if len(tpays) > 0: 

225 batch = Batch.objects.create() 

226 tpays.update(batch=batch) 

227 _show_message( 

228 self, 

229 request, 

230 len(tpays), 

231 _("Successfully added {} payments to new batch").format(len(tpays)), 

232 _("No payments using Thalia Pay are selected, no batch is created"), 

233 ) 

234 

235 add_to_new_batch.short_description = _( 

236 "Add selected Thalia Pay payments to a new batch" 

237 ) 

238 

239 def add_to_last_batch(self, request: HttpRequest, queryset: QuerySet) -> None: 

240 """Add selected TPAY payments to the last batch.""" 

241 tpays = queryset.filter(type=Payment.TPAY) 

242 if len(tpays) > 0: 

243 batch = Batch.objects.last() 

244 if batch is None: 

245 self.message_user(request, _("No batches available."), messages.ERROR) 

246 elif not batch.processed: 

247 batch.save() 

248 tpays.update(batch=batch) 

249 self.message_user( 

250 request, 

251 _("Successfully added {} payments to {}").format(len(tpays), batch), 

252 messages.SUCCESS, 

253 ) 

254 else: 

255 self.message_user( 

256 request, 

257 _("The last batch {} is already processed").format(batch), 

258 messages.ERROR, 

259 ) 

260 else: 

261 self.message_user( 

262 request, 

263 _("No payments using Thalia Pay are selected, no batch is created"), 

264 messages.ERROR, 

265 ) 

266 

267 add_to_last_batch.short_description = _( 

268 "Add selected Thalia Pay payments to the last batch" 

269 ) 

270 

271 def get_urls(self) -> list: 

272 urls = super().get_urls() 

273 custom_urls = [ 

274 path( 

275 "<str:app_label>/<str:model_name>/<payable>/create/", 

276 self.admin_site.admin_view(admin_views.PaymentAdminView.as_view()), 

277 name="payments_payment_create", 

278 ), 

279 ] 

280 return custom_urls + urls 

281 

282 def export_csv(self, request: HttpRequest, queryset: QuerySet) -> HttpResponse: 

283 """Export a CSV of payments. 

284 

285 :param request: Request 

286 :param queryset: Items to be exported 

287 """ 

288 response = HttpResponse(content_type="text/csv") 

289 response["Content-Disposition"] = 'attachment;filename="payments.csv"' 

290 writer = csv.writer(response) 

291 headers = [ 

292 _("created"), 

293 _("amount"), 

294 _("type"), 

295 _("processor"), 

296 _("payer id"), 

297 _("payer name"), 

298 _("notes"), 

299 ] 

300 writer.writerow([capfirst(x) for x in headers]) 

301 for payment in queryset: 

302 writer.writerow( 

303 [ 

304 payment.created_at, 

305 payment.amount, 

306 payment.get_type_display(), 

307 payment.processed_by.get_full_name() 

308 if payment.processed_by 

309 else "-", 

310 payment.paid_by.pk if payment.paid_by else "-", 

311 payment.paid_by.get_full_name() if payment.paid_by else "-", 

312 payment.notes, 

313 ] 

314 ) 

315 return response 

316 

317 export_csv.short_description = _("Export") 

318 

319 

320class ValidAccountFilter(admin.SimpleListFilter): 

321 """Filter the bank accounts by whether they are active or not.""" 

322 

323 title = _("mandates") 

324 parameter_name = "active" 

325 

326 def lookups(self, request, model_admin) -> tuple: 

327 return ( 

328 ("valid", _("Valid")), 

329 ("invalid", _("Invalid")), 

330 ("none", _("None")), 

331 ) 

332 

333 def queryset(self, request, queryset) -> QuerySet: 

334 today = timezone.now().date() 

335 

336 if self.value() == "valid": 

337 return queryset.filter( 

338 Q(valid_from__lte=today) 

339 & (Q(valid_until=None) | Q(valid_until__gt=today)) 

340 ) 

341 

342 if self.value() == "invalid": 

343 return queryset.filter( 

344 Q(valid_from__isnull=False) 

345 & (Q(valid_from__gt=today) | Q(valid_until__lte=today)) 

346 ) 

347 

348 if self.value() == "none": 

349 return queryset.filter(valid_from=None) 

350 

351 return queryset 

352 

353 

354class PaymentsInline(admin.TabularInline): 

355 """The inline for payments in the Batch admin.""" 

356 

357 model = Payment 

358 readonly_fields = ( 

359 "topic", 

360 "paid_by", 

361 "amount", 

362 "created_at", 

363 "notes", 

364 ) 

365 form = BatchPaymentInlineAdminForm 

366 extra = 0 

367 max_num = 0 

368 can_delete = False 

369 

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

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

372 if obj and obj.processed: 

373 fields.remove("remove_batch") 

374 return fields 

375 

376 

377@admin.register(Batch) 

378class BatchAdmin(admin.ModelAdmin): 

379 """Manage payment batches.""" 

380 

381 inlines = (PaymentsInline,) 

382 list_display = ( 

383 "id", 

384 "description", 

385 "withdrawal_date", 

386 "start_date", 

387 "end_date", 

388 "total_amount", 

389 "payments_count", 

390 "processing_date", 

391 "processed", 

392 ) 

393 fields = ( 

394 "id", 

395 "description", 

396 "withdrawal_date", 

397 "processed", 

398 "processing_date", 

399 "total_amount", 

400 ) 

401 search_fields = ( 

402 "id", 

403 "description", 

404 "withdrawal_date", 

405 ) 

406 

407 def get_readonly_fields(self, request: HttpRequest, obj: Batch = None): 

408 default_fields = ( 

409 "id", 

410 "processed", 

411 "processing_date", 

412 "total_amount", 

413 ) 

414 if obj and obj.processed: 

415 return ( 

416 "description", 

417 "withdrawal_date", 

418 ) + default_fields 

419 return default_fields 

420 

421 def has_delete_permission(self, request, obj=None): 

422 if isinstance(obj, Batch): 

423 if obj.processed: 

424 return False 

425 if ( 

426 "batch/" in request.path 

427 and request.POST 

428 and request.POST.get("action") == "delete_selected" 

429 ): 

430 for payment_id in request.POST.getlist("_selected_action"): 

431 if Batch.objects.get(id=payment_id).processed: 

432 return False 

433 

434 return super().has_delete_permission(request, obj) 

435 

436 def get_urls(self) -> list: 

437 urls = super().get_urls() 

438 custom_urls = [ 

439 path( 

440 "<int:pk>/process/", 

441 self.admin_site.admin_view(admin_views.BatchProcessAdminView.as_view()), 

442 name="payments_batch_process", 

443 ), 

444 path( 

445 "<int:pk>/export/", 

446 self.admin_site.admin_view(admin_views.BatchExportAdminView.as_view()), 

447 name="payments_batch_export", 

448 ), 

449 path( 

450 "<int:pk>/export-topic/", 

451 self.admin_site.admin_view( 

452 admin_views.BatchTopicExportAdminView.as_view() 

453 ), 

454 name="payments_batch_export_topic", 

455 ), 

456 path( 

457 "<int:pk>/topic-description/", 

458 self.admin_site.admin_view( 

459 admin_views.BatchTopicDescriptionAdminView.as_view() 

460 ), 

461 name="payments_batch_topic_description", 

462 ), 

463 path( 

464 "new_filled/", 

465 self.admin_site.admin_view( 

466 admin_views.BatchNewFilledAdminView.as_view() 

467 ), 

468 name="payments_batch_new_batch_filled", 

469 ), 

470 ] 

471 return custom_urls + urls 

472 

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

474 instances = formset.save(commit=False) 

475 

476 for instance in instances: 

477 if instance.batch and not instance.batch.processed: 

478 instance.batch = None 

479 instance.save() 

480 formset.save_m2m() 

481 

482 def changeform_view( 

483 self, 

484 request: HttpRequest, 

485 object_id: str | None = None, 

486 form_url: str = "", 

487 extra_context: dict | None = None, 

488 ) -> HttpResponse: 

489 """Render the change formview. 

490 

491 Only allow when the batch has not been processed yet. 

492 """ 

493 extra_context = extra_context or {} 

494 obj = None 

495 if object_id is not None and request.user.has_perm("payments.process_batches"): 

496 obj = Batch.objects.get(id=object_id) 

497 

498 extra_context["batch"] = obj 

499 return super().changeform_view(request, object_id, form_url, extra_context) 

500 

501 

502@admin.register(BankAccount) 

503class BankAccountAdmin(admin.ModelAdmin): 

504 """Manage bank accounts.""" 

505 

506 list_display = ("iban", "owner_link", "last_used", "valid_from", "valid_until") 

507 fields = ( 

508 "created_at", 

509 "last_used", 

510 "owner", 

511 "iban", 

512 "bic", 

513 "initials", 

514 "last_name", 

515 "mandate_no", 

516 "valid_from", 

517 "valid_until", 

518 "signature", 

519 "can_be_revoked", 

520 ) 

521 readonly_fields = ( 

522 "created_at", 

523 "can_be_revoked", 

524 ) 

525 search_fields = ("owner__username", "owner__first_name", "owner__last_name", "iban") 

526 autocomplete_fields = ("owner",) 

527 actions = ["set_last_used"] 

528 form = BankAccountAdminForm 

529 

530 def owner_link(self, obj: BankAccount) -> str: 

531 if obj.owner: 

532 return format_html( 

533 "<a href='{}'>{}</a>", 

534 reverse("admin:auth_user_change", args=[obj.owner.pk]), 

535 obj.owner.get_full_name(), 

536 ) 

537 return "" 

538 

539 owner_link.admin_order_field = "owner" 

540 owner_link.short_description = _("owner") 

541 

542 def can_be_revoked(self, obj: BankAccount): 

543 return obj.can_be_revoked 

544 

545 can_be_revoked.boolean = True 

546 

547 def set_last_used(self, request: HttpRequest, queryset: QuerySet) -> None: 

548 """Set the last used date of selected accounts.""" 

549 if request.user.has_perm("payments.change_bankaccount"): 

550 updated = services.update_last_used(queryset) 

551 _show_message( 

552 self, 

553 request, 

554 updated, 

555 message=_("Successfully updated %(count)d %(items)s."), 

556 error=_("The selected account(s) could not be updated."), 

557 ) 

558 

559 set_last_used.short_description = _("Update the last used date") 

560 

561 def export_csv(self, request: HttpRequest, queryset: QuerySet) -> HttpResponse: 

562 response = HttpResponse(content_type="text/csv") 

563 response["Content-Disposition"] = 'attachment;filename="accounts.csv"' 

564 writer = csv.writer(response) 

565 headers = [ 

566 _("created"), 

567 _("name"), 

568 _("reference"), 

569 _("IBAN"), 

570 _("BIC"), 

571 _("valid from"), 

572 _("valid until"), 

573 _("signature"), 

574 ] 

575 writer.writerow([capfirst(x) for x in headers]) 

576 for account in queryset: 

577 writer.writerow( 

578 [ 

579 account.created_at, 

580 account.name, 

581 account.mandate_no, 

582 account.iban, 

583 account.bic or "", 

584 account.valid_from or "", 

585 account.valid_until or "", 

586 account.signature or "", 

587 ] 

588 ) 

589 return response 

590 

591 export_csv.short_description = _("Export") 

592 

593 

594class BankAccountInline(admin.TabularInline): 

595 model = BankAccount 

596 fields = ( 

597 "iban", 

598 "bic", 

599 "mandate_no", 

600 "valid_from", 

601 "valid_until", 

602 "last_used", 

603 ) 

604 show_change_link = True 

605 

606 can_delete = False 

607 

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

609 return False 

610 

611 def has_change_permission(self, request, obj=None): 

612 return False 

613 

614 def has_delete_permission(self, request, obj=None): 

615 return False 

616 

617 

618class PaymentInline(admin.TabularInline): 

619 model = Payment 

620 fk_name = "paid_by" 

621 fields = ( 

622 "created_at", 

623 "type", 

624 "amount", 

625 "topic", 

626 "notes", 

627 "batch", 

628 ) 

629 ordering = ("-created_at",) 

630 

631 show_change_link = True 

632 

633 can_delete = False 

634 

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

636 return False 

637 

638 def has_change_permission(self, request, obj=None): 

639 return False 

640 

641 def has_delete_permission(self, request, obj=None): 

642 return False 

643 

644 

645class ThaliaPayAllowedFilter(admin.SimpleListFilter): 

646 title = _("Thalia Pay allowed") 

647 parameter_name = "tpay_allowed" 

648 

649 def lookups(self, request, model_admin): 

650 return ("1", _("Yes")), ("0", _("No")) 

651 

652 def queryset(self, request, queryset): 

653 if self.value() == "1": 

654 return queryset.filter(tpay_allowed=True) 

655 if self.value() == "0": 

656 return queryset.exclude(tpay_allowed=True) 

657 return queryset 

658 

659 

660class ThaliaPayEnabledFilter(admin.SimpleListFilter): 

661 title = _("Thalia Pay enabled") 

662 parameter_name = "tpay_enabled" 

663 

664 def lookups(self, request, model_admin): 

665 return ("1", _("Yes")), ("0", _("No")) 

666 

667 def queryset(self, request, queryset): 

668 if self.value() == "1": 

669 return queryset.filter(tpay_enabled=True) 

670 if self.value() == "0": 

671 return queryset.exclude(tpay_enabled=True) 

672 return queryset 

673 

674 

675class ThaliaPayBalanceFilter(admin.SimpleListFilter): 

676 title = _("Thalia Pay balance") 

677 parameter_name = "tpay_balance" 

678 

679 def lookups(self, request, model_admin): 

680 return ( 

681 ("0", "€0,00"), 

682 ("1", ">€0.00"), 

683 ) 

684 

685 def queryset(self, request, queryset): 

686 if self.value() == "0": 

687 return queryset.filter(tpay_balance=0) 

688 if self.value() == "1": 

689 return queryset.exclude(tpay_balance=0) 

690 return queryset 

691 

692 

693@admin.register(PaymentUser) 

694class PaymentUserAdmin(admin.ModelAdmin): 

695 list_display = ( 

696 "__str__", 

697 "email", 

698 "get_tpay_allowed", 

699 "get_tpay_enabled", 

700 "get_tpay_balance", 

701 ) 

702 list_filter = [ 

703 ThaliaPayAllowedFilter, 

704 ThaliaPayEnabledFilter, 

705 ThaliaPayBalanceFilter, 

706 ] 

707 

708 inlines = [BankAccountInline, PaymentInline] 

709 

710 fields = ( 

711 "user_link", 

712 "get_tpay_allowed", 

713 "get_tpay_enabled", 

714 "get_tpay_balance", 

715 ) 

716 

717 readonly_fields = ( 

718 "user_link", 

719 "get_tpay_allowed", 

720 "get_tpay_enabled", 

721 "get_tpay_balance", 

722 ) 

723 

724 search_fields = ( 

725 "first_name", 

726 "last_name", 

727 "username", 

728 "email", 

729 ) 

730 

731 # Facet counts would crash for this admin. 

732 show_facets = admin.ShowFacets.NEVER 

733 

734 def get_queryset(self, request): 

735 queryset = super().get_queryset(request) 

736 queryset = queryset.prefetch_related("bank_accounts", "paid_payment_set") 

737 queryset = queryset.select_properties( 

738 "tpay_balance", 

739 "tpay_enabled", 

740 "tpay_allowed", 

741 ) 

742 return queryset 

743 

744 def get_tpay_balance(self, obj): 

745 return f"{obj.tpay_balance:.2f}" if obj.tpay_enabled else "-" 

746 

747 get_tpay_balance.short_description = _("balance") 

748 

749 def get_tpay_enabled(self, obj): 

750 return obj.tpay_enabled 

751 

752 get_tpay_enabled.short_description = _("Thalia Pay enabled") 

753 get_tpay_enabled.boolean = True 

754 

755 def get_tpay_allowed(self, obj): 

756 return obj.tpay_allowed 

757 

758 get_tpay_allowed.short_description = _("Thalia Pay allowed") 

759 get_tpay_allowed.boolean = True 

760 

761 def user_link(self, obj): 

762 return ( 

763 format_html( 

764 "<a href='{}'>{}</a>", 

765 reverse("admin:auth_user_change", args=[obj.pk]), 

766 obj.get_full_name(), 

767 ) 

768 if obj 

769 else "" 

770 ) 

771 

772 user_link.admin_order_field = "user" 

773 user_link.short_description = _("user") 

774 

775 actions = ["disallow_thalia_pay", "allow_thalia_pay"] 

776 

777 def disallow_thalia_pay(self, request, queryset): 

778 count = 0 

779 for x in queryset: 

780 changed = x.disallow_tpay() 

781 count += 1 if changed else 0 

782 messages.success( 

783 request, f"Succesfully disallowed Thalia Pay for {count} users." 

784 ) 

785 

786 disallow_thalia_pay.short_description = _("Disallow Thalia Pay for selected users") 

787 

788 def allow_thalia_pay(self, request, queryset): 

789 count = 0 

790 for x in queryset: 

791 changed = x.allow_tpay() 

792 count += 1 if changed else 0 

793 messages.success(request, f"Succesfully allowed Thalia Pay for {count} users.") 

794 

795 allow_thalia_pay.short_description = _("Allow Thalia Pay for selected users") 

796 

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

798 return False 

799 

800 def has_change_permission(self, request, obj=None): 

801 return False 

802 

803 def has_delete_permission(self, request, obj=None): 

804 return False