Skip to content

Commit 374ae22

Browse files
committed
Add django model and migration for grant reimbursement
This change introduces a new GrantReimbursement model and related migration to support flexible grant reimbursement categories. The model links grants to their reimbursement categories and stores the granted amount for each. Key changes: - Add `GrantReimbursement` model as through table for M2M relationship - Add `reimbursement_categories` M2M field to `Grant` model - Create migration for the new model and relationship - Add factory for testing This change enables more granular control over grant reimbursements by allowing different categories (e.g., travel, accommodation) with specific amounts for each category.
1 parent 037f5d3 commit 374ae22

File tree

4 files changed

+97
-2
lines changed

4 files changed

+97
-2
lines changed

backend/grants/admin.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@
3030
)
3131
from schedule.models import ScheduleItem
3232
from submissions.models import Submission
33-
from .models import Grant, GrantConfirmPendingStatusProxy, GrantReimbursementCategory
33+
from .models import (
34+
Grant,
35+
GrantConfirmPendingStatusProxy,
36+
GrantReimbursementCategory,
37+
GrantReimbursement,
38+
)
3439
from django.db.models import Exists, OuterRef, F
3540
from pretix import user_has_admission_ticket
3641

@@ -399,6 +404,18 @@ class GrantReimbursementCategoryAdmin(ConferencePermissionMixin, admin.ModelAdmi
399404
list_filter = ("conference", "category", "included_by_default")
400405

401406

407+
@admin.register(GrantReimbursement)
408+
class GrantReimbursementAdmin(ConferencePermissionMixin, admin.ModelAdmin):
409+
list_display = (
410+
"grant",
411+
"category",
412+
"granted_amount",
413+
)
414+
list_filter = ("grant__conference", "category")
415+
search_fields = ("grant__full_name", "grant__email")
416+
autocomplete_fields = ("grant",)
417+
418+
402419
@admin.register(Grant)
403420
class GrantAdmin(ExportMixin, ConferencePermissionMixin, admin.ModelAdmin):
404421
change_list_template = "admin/grants/grant/change_list.html"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 5.1.4 on 2025-06-04 16:50
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('grants', '0029_grantreimbursementcategory'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='GrantReimbursement',
16+
fields=[
17+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
('granted_amount', models.DecimalField(decimal_places=0, help_text='Actual amount granted for this category', max_digits=6, verbose_name='granted amount')),
19+
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='grants.grantreimbursementcategory', verbose_name='reimbursement category')),
20+
('grant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reimbursements', to='grants.grant', verbose_name='grant')),
21+
],
22+
options={
23+
'verbose_name': 'Grant Reimbursement',
24+
'verbose_name_plural': 'Grant Reimbursements',
25+
'ordering': ['grant', 'category'],
26+
'unique_together': {('grant', 'category')},
27+
},
28+
),
29+
migrations.AddField(
30+
model_name='grant',
31+
name='reimbursement_categories',
32+
field=models.ManyToManyField(related_name='grants', through='grants.GrantReimbursement', to='grants.grantreimbursementcategory'),
33+
),
34+
]

backend/grants/models.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,10 @@ class ApprovedType(models.TextChoices):
250250
blank=True,
251251
)
252252

253+
reimbursement_categories = models.ManyToManyField(
254+
GrantReimbursementCategory, through="GrantReimbursement", related_name="grants"
255+
)
256+
253257
objects = GrantQuerySet().as_manager()
254258

255259
def __init__(self, *args, **kwargs):
@@ -370,6 +374,37 @@ def has_approved_accommodation(self):
370374
)
371375

372376

377+
class GrantReimbursement(models.Model):
378+
"""Links a Grant to its reimbursement categories and stores the actual amount granted."""
379+
380+
grant = models.ForeignKey(
381+
Grant,
382+
on_delete=models.CASCADE,
383+
related_name="reimbursements",
384+
verbose_name=_("grant"),
385+
)
386+
category = models.ForeignKey(
387+
GrantReimbursementCategory,
388+
on_delete=models.CASCADE,
389+
verbose_name=_("reimbursement category"),
390+
)
391+
granted_amount = models.DecimalField(
392+
_("granted amount"),
393+
max_digits=6,
394+
decimal_places=0,
395+
help_text=_("Actual amount granted for this category"),
396+
)
397+
398+
def __str__(self):
399+
return f"{self.grant.full_name} - {self.category.name} - {self.granted_amount}"
400+
401+
class Meta:
402+
verbose_name = _("Grant Reimbursement")
403+
verbose_name_plural = _("Grant Reimbursements")
404+
unique_together = [("grant", "category")]
405+
ordering = ["grant", "category"]
406+
407+
373408
class GrantConfirmPendingStatusProxy(Grant):
374409
class Meta:
375410
proxy = True

backend/grants/tests/factories.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from factory.django import DjangoModelFactory
33

44
from conferences.tests.factories import ConferenceFactory
5-
from grants.models import Grant, GrantReimbursementCategory
5+
from grants.models import Grant, GrantReimbursementCategory, GrantReimbursement
66
from helpers.constants import GENDERS
77
from users.tests.factories import UserFactory
88
from countries import countries
@@ -75,3 +75,12 @@ def _create(self, model_class, *args, **kwargs):
7575
ParticipantFactory(user_id=grant.user.id, conference=grant.conference)
7676

7777
return grant
78+
79+
80+
class GrantReimbursementFactory(DjangoModelFactory):
81+
class Meta:
82+
model = GrantReimbursement
83+
84+
grant = factory.SubFactory(GrantFactory)
85+
category = factory.SubFactory(GrantReimbursementCategoryFactory)
86+
granted_amount = factory.LazyAttribute(lambda obj: obj.category.max_amount)

0 commit comments

Comments
 (0)