11import logging
2- from django .db import transaction
3- from custom_admin .audit import (
4- create_addition_admin_log_entry ,
5- create_change_admin_log_entry ,
6- )
7- from conferences .models .conference_voucher import ConferenceVoucher
8- from pycon .constants import UTC
9- from custom_admin .admin import (
10- confirm_pending_status ,
11- reset_pending_status_back_to_status ,
12- validate_single_conference_selection ,
13- )
14- from import_export .resources import ModelResource
152from datetime import timedelta
163from typing import Dict , List , Optional
17- from countries . filters import CountryFilter
4+
185from django .contrib import admin , messages
6+ from django .contrib .admin import SimpleListFilter
7+ from django .db import transaction
8+ from django .db .models import Exists , OuterRef
199from django .db .models .query import QuerySet
10+ from django .urls import reverse
2011from django .utils import timezone
12+ from django .utils .safestring import mark_safe
2113from import_export .admin import ExportMixin
2214from import_export .fields import Field
23- from users .admin_mixins import ConferencePermissionMixin
15+ from import_export .resources import ModelResource
16+
17+ from conferences .models .conference_voucher import ConferenceVoucher
2418from countries import countries
19+ from countries .filters import CountryFilter
20+ from custom_admin .admin import (
21+ confirm_pending_status ,
22+ reset_pending_status_back_to_status ,
23+ validate_single_conference_selection ,
24+ )
25+ from custom_admin .audit import (
26+ create_addition_admin_log_entry ,
27+ create_change_admin_log_entry ,
28+ )
2529from grants .tasks import (
2630 send_grant_reply_approved_email ,
31+ send_grant_reply_rejected_email ,
2732 send_grant_reply_waiting_list_email ,
2833 send_grant_reply_waiting_list_update_email ,
29- send_grant_reply_rejected_email ,
3034)
35+ from participants .models import Participant
36+ from pretix import user_has_admission_ticket
37+ from pycon .constants import UTC
3138from schedule .models import ScheduleItem
3239from submissions .models import Submission
33- from .models import Grant , GrantConfirmPendingStatusProxy
34- from django .db .models import Exists , OuterRef
35- from pretix import user_has_admission_ticket
36-
37- from django .contrib .admin import SimpleListFilter
38- from participants .models import Participant
39- from django .urls import reverse
40- from django .utils .safestring import mark_safe
40+ from users .admin_mixins import ConferencePermissionMixin
4141from visa .models import InvitationLetterRequest
4242
43+ from .models import (
44+ Grant ,
45+ GrantConfirmPendingStatusProxy ,
46+ GrantReimbursement ,
47+ GrantReimbursementCategory ,
48+ )
49+
4350logger = logging .getLogger (__name__ )
4451
4552EXPORT_GRANTS_FIELDS = (
@@ -164,27 +171,13 @@ class Meta:
164171
165172
166173def _check_amounts_are_not_empty (grant : Grant , request ):
167- if grant .total_amount is None :
174+ if grant .total_allocated_amount == 0 :
168175 messages .error (
169176 request ,
170177 f"Grant for { grant .name } is missing 'Total Amount'!" ,
171178 )
172179 return False
173180
174- if grant .has_approved_accommodation () and grant .accommodation_amount is None :
175- messages .error (
176- request ,
177- f"Grant for { grant .name } is missing 'Accommodation Amount'!" ,
178- )
179- return False
180-
181- if grant .has_approved_travel () and grant .travel_amount is None :
182- messages .error (
183- request ,
184- f"Grant for { grant .name } is missing 'Travel Amount'!" ,
185- )
186- return False
187-
188181 return True
189182
190183
@@ -208,10 +201,10 @@ def send_reply_emails(modeladmin, request, queryset):
208201
209202 for grant in queryset :
210203 if grant .status in (Grant .Status .approved ,):
211- if grant .approved_type is None :
204+ if not grant .reimbursements . exists () :
212205 messages .error (
213206 request ,
214- f"Grant for { grant .name } is missing 'Grant Approved Type' !" ,
207+ f"Grant for { grant .name } is missing reimbursement categories !" ,
215208 )
216209 return
217210
@@ -394,6 +387,32 @@ def queryset(self, request, queryset):
394387 return queryset
395388
396389
390+ @admin .register (GrantReimbursementCategory )
391+ class GrantReimbursementCategoryAdmin (ConferencePermissionMixin , admin .ModelAdmin ):
392+ list_display = ("__str__" , "max_amount" , "category" , "included_by_default" )
393+ list_filter = ("conference" , "category" , "included_by_default" )
394+ search_fields = ("category" , "name" )
395+
396+
397+ @admin .register (GrantReimbursement )
398+ class GrantReimbursementAdmin (ConferencePermissionMixin , admin .ModelAdmin ):
399+ list_display = (
400+ "grant" ,
401+ "category" ,
402+ "granted_amount" ,
403+ )
404+ list_filter = ("grant__conference" , "category" )
405+ search_fields = ("grant__full_name" , "grant__email" )
406+ autocomplete_fields = ("grant" ,)
407+
408+
409+ class GrantReimbursementInline (admin .TabularInline ):
410+ model = GrantReimbursement
411+ extra = 0
412+ autocomplete_fields = ["category" ]
413+ fields = ["category" , "granted_amount" ]
414+
415+
397416@admin .register (Grant )
398417class GrantAdmin (ExportMixin , ConferencePermissionMixin , admin .ModelAdmin ):
399418 change_list_template = "admin/grants/grant/change_list.html"
@@ -406,12 +425,8 @@ class GrantAdmin(ExportMixin, ConferencePermissionMixin, admin.ModelAdmin):
406425 "has_sent_invitation_letter_request" ,
407426 "emoji_gender" ,
408427 "conference" ,
409- "status" ,
410- "approved_type" ,
411- "ticket_amount" ,
412- "travel_amount" ,
413- "accommodation_amount" ,
414- "total_amount" ,
428+ "current_or_pending_status" ,
429+ "total_amount_display" ,
415430 "country_type" ,
416431 "user_has_ticket" ,
417432 "has_voucher" ,
@@ -425,7 +440,6 @@ class GrantAdmin(ExportMixin, ConferencePermissionMixin, admin.ModelAdmin):
425440 "pending_status" ,
426441 "country_type" ,
427442 "occupation" ,
428- "approved_type" ,
429443 "needs_funds_for_travel" ,
430444 "need_visa" ,
431445 "need_accommodation" ,
@@ -451,6 +465,7 @@ class GrantAdmin(ExportMixin, ConferencePermissionMixin, admin.ModelAdmin):
451465 "delete_selected" ,
452466 ]
453467 autocomplete_fields = ("user" ,)
468+ inlines = [GrantReimbursementInline ]
454469
455470 fieldsets = (
456471 (
@@ -459,12 +474,7 @@ class GrantAdmin(ExportMixin, ConferencePermissionMixin, admin.ModelAdmin):
459474 "fields" : (
460475 "status" ,
461476 "pending_status" ,
462- "approved_type" ,
463477 "country_type" ,
464- "ticket_amount" ,
465- "travel_amount" ,
466- "accommodation_amount" ,
467- "total_amount" ,
468478 "applicant_reply_sent_at" ,
469479 "applicant_reply_deadline" ,
470480 "internal_notes" ,
@@ -528,6 +538,10 @@ def user_display_name(self, obj):
528538 return obj .user .display_name
529539 return obj .email
530540
541+ @admin .display (description = "Status" )
542+ def current_or_pending_status (self , obj ):
543+ return obj .current_or_pending_status
544+
531545 @admin .display (
532546 description = "C" ,
533547 )
@@ -592,10 +606,22 @@ def has_sent_invitation_letter_request(self, obj: Grant) -> bool:
592606 return "📧"
593607 return ""
594608
609+ @admin .display (description = "Total" )
610+ def total_amount_display (self , obj ):
611+ return f"{ obj .total_allocated_amount :.2f} "
612+
613+ @admin .display (description = "Approved Reimbursements" )
614+ def approved_amounts_display (self , obj ):
615+ return ", " .join (
616+ f"{ r .category .name } : { r .granted_amount } " for r in obj .reimbursements .all ()
617+ )
618+
595619 def get_queryset (self , request ):
596620 qs = (
597621 super ()
598622 .get_queryset (request )
623+ .select_related ("user" )
624+ .prefetch_related ("reimbursements__category" )
599625 .annotate (
600626 is_proposed_speaker = Exists (
601627 Submission .objects .non_cancelled ().filter (
0 commit comments