Skip to content

Commit 037f5d3

Browse files
committed
Add flexible grant reimbursement categories
Replace the hardcoded grant reimbursement types with a configurable model. Each conference can now define its own reimbursement categories with custom: - Names and descriptions - Maximum amounts per category - Default inclusion settings - Category types (travel, ticket, accommodation, other) This allows for more flexible grant management where each conference can customize their reimbursement options while maintaining data consistency through the category system. It replaces the old hardcoded `ApprovedType` choices with a dynamic model-based approach.
1 parent 146c9bf commit 037f5d3

File tree

4 files changed

+96
-2
lines changed

4 files changed

+96
-2
lines changed

backend/grants/admin.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
)
3131
from schedule.models import ScheduleItem
3232
from submissions.models import Submission
33-
from .models import Grant, GrantConfirmPendingStatusProxy
33+
from .models import Grant, GrantConfirmPendingStatusProxy, GrantReimbursementCategory
3434
from django.db.models import Exists, OuterRef, F
3535
from pretix import user_has_admission_ticket
3636

@@ -393,6 +393,12 @@ def queryset(self, request, queryset):
393393
return queryset
394394

395395

396+
@admin.register(GrantReimbursementCategory)
397+
class GrantReimbursementCategoryAdmin(ConferencePermissionMixin, admin.ModelAdmin):
398+
list_display = ("__str__", "max_amount", "category", "included_by_default")
399+
list_filter = ("conference", "category", "included_by_default")
400+
401+
396402
@admin.register(Grant)
397403
class GrantAdmin(ExportMixin, ConferencePermissionMixin, admin.ModelAdmin):
398404
change_list_template = "admin/grants/grant/change_list.html"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Generated by Django 5.1.4 on 2025-06-04 15:20
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+
('conferences', '0054_conference_frontend_revalidate_secret_and_more'),
11+
('grants', '0028_remove_grant_pretix_voucher_id_and_more'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='GrantReimbursementCategory',
17+
fields=[
18+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('name', models.CharField(max_length=100)),
20+
('description', models.TextField(blank=True, null=True)),
21+
('max_amount', models.DecimalField(decimal_places=0, help_text='Maximum amount for this category', max_digits=6)),
22+
('category', models.CharField(choices=[('travel', 'Travel'), ('ticket', 'Ticket'), ('accommodation', 'Accommodation'), ('other', 'Other')], max_length=20)),
23+
('included_by_default', models.BooleanField(default=False, help_text='Automatically include this category in grants by default')),
24+
('conference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reimbursement_categories', to='conferences.conference')),
25+
],
26+
options={
27+
'verbose_name': 'Grant Reimbursement Category',
28+
'verbose_name_plural': 'Grant Reimbursement Categories',
29+
'ordering': ['conference', 'category'],
30+
'unique_together': {('conference', 'category')},
31+
},
32+
),
33+
]

backend/grants/models.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,43 @@ def of_user(self, user):
1414
return self.filter(user=user)
1515

1616

17+
class GrantReimbursementCategory(models.Model):
18+
"""
19+
Define types of reimbursements available for a grant (e.g., Travel, Ticket, Accommodation).
20+
"""
21+
22+
class Category(models.TextChoices):
23+
TRAVEL = "travel", _("Travel")
24+
TICKET = "ticket", _("Ticket")
25+
ACCOMMODATION = "accommodation", _("Accommodation")
26+
OTHER = "other", _("Other")
27+
28+
conference = models.ForeignKey(
29+
"conferences.Conference",
30+
on_delete=models.CASCADE,
31+
related_name="reimbursement_categories",
32+
)
33+
name = models.CharField(max_length=100)
34+
description = models.TextField(blank=True, null=True)
35+
max_amount = models.DecimalField(
36+
max_digits=6, decimal_places=0, help_text=_("Maximum amount for this category")
37+
)
38+
category = models.CharField(max_length=20, choices=Category.choices)
39+
included_by_default = models.BooleanField(
40+
default=False,
41+
help_text="Automatically include this category in grants by default",
42+
)
43+
44+
def __str__(self):
45+
return f"{self.name} ({self.conference.name})"
46+
47+
class Meta:
48+
verbose_name = _("Grant Reimbursement Category")
49+
verbose_name_plural = _("Grant Reimbursement Categories")
50+
unique_together = [("conference", "category")]
51+
ordering = ["conference", "category"]
52+
53+
1754
class Grant(TimeStampedModel):
1855
# TextChoices
1956
class Status(models.TextChoices):

backend/grants/tests/factories.py

Lines changed: 19 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
5+
from grants.models import Grant, GrantReimbursementCategory
66
from helpers.constants import GENDERS
77
from users.tests.factories import UserFactory
88
from countries import countries
@@ -11,6 +11,24 @@
1111
import random
1212

1313

14+
class GrantReimbursementCategoryFactory(DjangoModelFactory):
15+
"""
16+
Factory for creating GrantReimbursementCategory instances for testing.
17+
"""
18+
19+
class Meta:
20+
model = GrantReimbursementCategory
21+
22+
conference = factory.SubFactory(ConferenceFactory)
23+
name = factory.Faker("word")
24+
description = factory.Faker("sentence")
25+
max_amount = factory.fuzzy.FuzzyDecimal(50, 500, precision=2)
26+
category = factory.fuzzy.FuzzyChoice(
27+
GrantReimbursementCategory.CategoryType.choices, getter=lambda x: x[0]
28+
)
29+
included_by_default = factory.Faker("boolean")
30+
31+
1432
class GrantFactory(DjangoModelFactory):
1533
class Meta:
1634
model = Grant

0 commit comments

Comments
 (0)