From 7c415557b2163b914a58c123e6d710542380d9ed Mon Sep 17 00:00:00 2001 From: Dirk Doesburg Date: Sat, 9 Sep 2023 16:01:02 +0200 Subject: [PATCH] Implement reimbursements --- website/miniconcrexit/templates/base.html | 4 ++ website/reimbursements/admin.py | 41 ++++++++++++- .../reimbursements/migrations/0001_initial.py | 60 +++++++++++++++++++ website/reimbursements/models.py | 36 ++++++++++- .../templates/reimbursements/create.html | 10 ++++ .../templates/reimbursements/index.html | 41 +++++++++++++ website/reimbursements/urls.py | 6 +- website/reimbursements/views.py | 32 +++++++++- 8 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 website/reimbursements/migrations/0001_initial.py create mode 100644 website/reimbursements/templates/reimbursements/create.html create mode 100644 website/reimbursements/templates/reimbursements/index.html diff --git a/website/miniconcrexit/templates/base.html b/website/miniconcrexit/templates/base.html index 04ddf05..328534e 100644 --- a/website/miniconcrexit/templates/base.html +++ b/website/miniconcrexit/templates/base.html @@ -20,6 +20,10 @@ Home + {% if user.is_authenticated %} + Reimbursements + {% endif %} + {% if user.is_staff %} Admin {% endif %} diff --git a/website/reimbursements/admin.py b/website/reimbursements/admin.py index fe90052..92b955f 100644 --- a/website/reimbursements/admin.py +++ b/website/reimbursements/admin.py @@ -1 +1,40 @@ -# Create an admin interface for your model here. +from django.contrib import admin +from django.contrib.admin import register +from django.utils import timezone +from reimbursements import models + + +@register(models.Reimbursement) +class ReimbursementAdmin(admin.ModelAdmin): + list_display = ( + "id", + "created", + "owner", + "date_incurred", + "amount", + "approved", + ) + list_filter = ("approved", "created", "date_incurred", "owner") + + autocomplete_fields = ["owner"] + readonly_fields = ["approved_by", "approved_at", "created", "updated"] + + def save_model(self, request, obj, form, change): + if obj.approved and not form.initial["approved"]: + obj.approved = True + obj.approved_by = request.user + obj.approved_at = timezone.now() + + super().save_model(request, obj, form, change) + + def get_readonly_fields(self, request, obj=None): + if obj and obj.approved: + return self.readonly_fields + [ + "approved", + "amount", + "description", + "receipt", + "date_incurred", + "owner", + ] + return self.readonly_fields diff --git a/website/reimbursements/migrations/0001_initial.py b/website/reimbursements/migrations/0001_initial.py new file mode 100644 index 0000000..37ad045 --- /dev/null +++ b/website/reimbursements/migrations/0001_initial.py @@ -0,0 +1,60 @@ +# Generated by Django 4.2.5 on 2023-09-09 20:11 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Reimbursement", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("amount", models.DecimalField(decimal_places=2, max_digits=10)), + ("date_incurred", models.DateField()), + ("description", models.TextField()), + ("receipt", models.FileField(upload_to="receipts/")), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ("approved", models.BooleanField(default=False)), + ("approved_at", models.DateTimeField(null=True)), + ( + "approved_by", + models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="reimbursements_approved", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "owner", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="reimbursements", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["created"], + }, + ), + ] diff --git a/website/reimbursements/models.py b/website/reimbursements/models.py index ffd95ee..68fe51a 100644 --- a/website/reimbursements/models.py +++ b/website/reimbursements/models.py @@ -1 +1,35 @@ -# Create a model here. +from django.db import models + + +class Reimbursement(models.Model): + owner = models.ForeignKey( + "auth.User", + related_name="reimbursements", + on_delete=models.SET_NULL, + null=True, + blank=False, + ) + + amount = models.DecimalField(max_digits=10, decimal_places=2) + date_incurred = models.DateField() + description = models.TextField() + receipt = models.FileField(upload_to="receipts/") + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + approved = models.BooleanField(default=False) + approved_at = models.DateTimeField(null=True) + approved_by = models.ForeignKey( + "auth.User", + related_name="reimbursements_approved", + on_delete=models.SET_NULL, + editable=False, + null=True, + ) + + class Meta: + ordering = ["created"] + + def __str__(self): + return f"Reimbursement #{self.id}" diff --git a/website/reimbursements/templates/reimbursements/create.html b/website/reimbursements/templates/reimbursements/create.html new file mode 100644 index 0000000..abdbbea --- /dev/null +++ b/website/reimbursements/templates/reimbursements/create.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} +{% load django_bootstrap5 %} +{% block body %} +

Request reimbursement

+
+ {% csrf_token %} + {% bootstrap_form form %} + {% bootstrap_button button_type="submit" content="Submit" %} +
+{% endblock %} \ No newline at end of file diff --git a/website/reimbursements/templates/reimbursements/index.html b/website/reimbursements/templates/reimbursements/index.html new file mode 100644 index 0000000..eca9871 --- /dev/null +++ b/website/reimbursements/templates/reimbursements/index.html @@ -0,0 +1,41 @@ +{% extends 'base.html' %} +{% block body %} +

My reimbursements

+

+ If you have made costs for Thalia, you can submit them here to request reimbursement. + Please note that you can only submit costs for which you have a receipt. + If you have any questions, please contact the treasurer. +

+ Submit new reimbursement + + {% if object_list %} + + + + + + + + + + + + + {% for object in object_list %} + + + + + + + + + {% endfor %} + +
#CreatedDate incurredDescriptionAmountApproved
{{ object.id }}{{ object.created }}{{ object.date_incurred }}{{ object.description }}€ {{ object.amount }}{% if object.approved %}{{ object.approved_at }}{% else %}Pending{% endif %}
+ {% else %} + + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/website/reimbursements/urls.py b/website/reimbursements/urls.py index 1886d17..bbb469b 100644 --- a/website/reimbursements/urls.py +++ b/website/reimbursements/urls.py @@ -1,5 +1,9 @@ from django.urls import path +from reimbursements.views import CreateReimbursementView, IndexView app_name = "reimbursements" -urlpatterns = [] +urlpatterns = [ + path("", IndexView.as_view(), name="index"), + path("create/", CreateReimbursementView.as_view(), name="create"), +] diff --git a/website/reimbursements/views.py b/website/reimbursements/views.py index a5243dc..f9a70ce 100644 --- a/website/reimbursements/views.py +++ b/website/reimbursements/views.py @@ -1 +1,31 @@ -# Write your views here. Create templates in `reimbursements/templates/reimbursements/`. +from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse_lazy +from django.views.generic import CreateView, ListView +from reimbursements.models import Reimbursement + + +class IndexView(LoginRequiredMixin, ListView): + model = Reimbursement + template_name = "reimbursements/index.html" + + def get_queryset(self): + return super().get_queryset().filter(owner=self.request.user) + + +class CreateReimbursementView(CreateView): + model = Reimbursement + template_name = "reimbursements/create.html" + + fields = ["date_incurred", "amount", "description", "receipt"] + + success_url = reverse_lazy("reimbursements:index") + + def form_valid(self, form): + form.instance.owner = self.request.user + form.save() + return super().form_valid(form) + + def get_context_data(self, **kwargs): + context = super().get_context_data() + context["form"].fields["date_incurred"].widget.input_type = "date" + return context