Files
claims-system/claims/views.py
2025-11-09 01:27:54 +01:00

361 lines
15 KiB
Python

from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.forms import formset_factory
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _
from django.views import View
from django.views.generic import ListView, TemplateView
from .forms import (
ClaimDecisionForm,
ClaimLineForm,
ClaimantForm,
DeleteUserForm,
UserManagementForm,
UserPermissionForm,
)
from .email_utils import notify_admin_of_claim, send_claimant_confirmation_email
from .models import Claim, ClaimLog, SystemSetting
User = get_user_model()
class SubmitClaimView(View):
template_name = "claims/submit_claim.html"
max_extra_forms = 5
def get_extra_forms(self):
try:
count = int(self.request.GET.get("forms", 2))
except (TypeError, ValueError):
count = 2
return max(1, min(count, self.max_extra_forms))
def build_formset(self, *, data=None, files=None, extra=0):
FormSet = formset_factory(
ClaimLineForm,
extra=extra,
min_num=1,
validate_min=True,
)
return FormSet(data=data, files=files, prefix="claim_lines")
def get_claimant_initial(self):
initial = {}
user = self.request.user
if user.is_authenticated:
initial["full_name"] = user.get_full_name() or user.get_username()
initial["email"] = user.email
last_claim = user.claims_submitted.order_by("-created_at").first()
if last_claim:
initial["account_number"] = last_claim.account_number
return initial
def build_context(self, formset, claimant_form):
current_forms = formset.total_form_count()
return {
"formset": formset,
"claimant_form": claimant_form,
"current_forms": current_forms,
"max_extra_forms": self.max_extra_forms,
"can_add_forms": current_forms < self.max_extra_forms,
"can_remove_forms": current_forms > 1,
"add_forms_value": min(self.max_extra_forms, current_forms + 1),
"remove_forms_value": max(1, current_forms - 1),
"form_fragment": "claim-formset",
}
def get(self, request):
extra = self.get_extra_forms()
formset = self.build_formset(extra=extra)
claimant_form = ClaimantForm(initial=self.get_claimant_initial())
context = self.build_context(formset, claimant_form)
if self._wants_fragment(request):
return render(request, "claims/includes/claim_formset.html", context)
return render(request, self.template_name, context)
def post(self, request):
formset = self.build_formset(data=request.POST, files=request.FILES)
claimant_form = ClaimantForm(request.POST)
if formset.is_valid() and claimant_form.is_valid():
claimant_data = claimant_form.cleaned_data
created = 0
for form in formset:
if not form.cleaned_data:
continue
description = form.cleaned_data.get("description")
amount = form.cleaned_data.get("amount")
if not description or amount is None:
continue
claim = Claim(
full_name=claimant_data["full_name"],
email=claimant_data["email"],
account_number=claimant_data["account_number"],
amount=amount,
currency=form.cleaned_data.get("currency") or Claim.Currency.SEK,
description=description,
receipt=form.cleaned_data.get("receipt"),
project=form.cleaned_data.get("project"),
submitted_by=request.user if request.user.is_authenticated else None,
)
claim.save()
claim.add_log(
action=ClaimLog.Action.CREATED,
performed_by=claim.submitted_by,
to_status=Claim.Status.PENDING,
)
send_claimant_confirmation_email(claim)
notify_admin_of_claim(claim)
created += 1
if created:
messages.success(request, _("{} utlägg skickade in.").format(created))
return redirect(reverse("claims:submit-success"))
messages.error(request, _("Inga utlägg kunde sparas. Fyll i minst en rad."))
else:
messages.error(request, _("Kunde inte spara utläggen. Kontrollera formuläret."))
return render(request, self.template_name, self.build_context(formset, claimant_form))
@staticmethod
def _wants_fragment(request):
return (
request.headers.get("x-requested-with") == "XMLHttpRequest"
or request.GET.get("fragment") == "claim-formset"
)
class ClaimAdminListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
template_name = "claims/admin_list.html"
context_object_name = "claims"
permission_required = "claims.view_claim"
def get_queryset(self):
queryset = (
Claim.objects.select_related("submitted_by", "project", "paid_by")
.prefetch_related("logs__performed_by")
.all()
)
status = self.request.GET.get("status")
if status in {choice[0] for choice in Claim.Status.choices}:
queryset = queryset.filter(status=status)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["status_filter"] = self.request.GET.get("status", "all")
context["status_choices"] = Claim.Status.choices
context["decision_choices"] = ClaimDecisionForm().fields["action"].choices
context["can_change"] = self.request.user.has_perm("claims.change_claim")
context["payments_enabled"] = SystemSetting.internal_payments_active()
return context
def post(self, request, *args, **kwargs):
action_type = request.POST.get("action_type", "decision")
if action_type == "payment":
return self._handle_payment(request)
return self._handle_decision(request)
def _handle_decision(self, request):
if not request.user.has_perm("claims.change_claim"):
messages.error(request, _("Du har inte behörighet att uppdatera utlägg."))
return redirect(request.get_full_path())
form = ClaimDecisionForm(request.POST)
if not form.is_valid():
for field_errors in form.errors.values():
for error in field_errors:
messages.error(request, error)
return redirect(request.get_full_path())
claim = get_object_or_404(Claim, pk=form.cleaned_data["claim_id"])
action = form.cleaned_data["action"]
decision_note = form.cleaned_data.get("decision_note", "")
if claim.is_paid:
messages.error(request, _("Utlägget är redan markerat som betalt och kan inte ändras."))
return redirect(request.get_full_path())
previous_status = claim.status
claim.decision_note = decision_note
if action == ClaimDecisionForm.ACTION_APPROVE:
claim.status = Claim.Status.APPROVED
messages.success(request, _("%(claim)s markerades som godkänd.") % {"claim": claim})
else:
claim.status = Claim.Status.REJECTED
messages.warning(request, _("%(claim)s markerades som nekad.") % {"claim": claim})
claim.save(update_fields=["status", "decision_note", "updated_at"])
claim.add_log(
action=ClaimLog.Action.STATUS_CHANGED,
performed_by=request.user,
from_status=previous_status,
to_status=claim.status,
note=decision_note,
)
return redirect(request.get_full_path())
def _handle_payment(self, request):
if not SystemSetting.internal_payments_active():
messages.error(request, _("Betalningshantering är inte aktiverad."))
return redirect(request.get_full_path())
if not request.user.has_perm("claims.change_claim"):
messages.error(request, _("Du har inte behörighet att uppdatera utlägg."))
return redirect(request.get_full_path())
claim = get_object_or_404(Claim, pk=request.POST.get("payment_claim_id"))
if claim.status != Claim.Status.APPROVED:
messages.error(request, _("Endast godkända utlägg kan markeras som betalda."))
return redirect(request.get_full_path())
if claim.is_paid:
messages.info(request, _("Detta utlägg är redan markerat som betalt."))
return redirect(request.get_full_path())
claim.paid_by = request.user
claim.paid_at = timezone.now()
claim.save(update_fields=["paid_by", "paid_at"])
claim.add_log(
action=ClaimLog.Action.MARKED_PAID,
performed_by=request.user,
note="Markerad som betald via systemet.",
)
messages.success(request, _("%(claim)s markerades som betald.") % {"claim": claim})
return redirect(request.get_full_path())
class ClaimExportMenuView(LoginRequiredMixin, PermissionRequiredMixin, TemplateView):
template_name = "claims/export_placeholder.html"
permission_required = "claims.view_claim"
class MyClaimsView(LoginRequiredMixin, ListView):
template_name = "claims/my_claims.html"
context_object_name = "claims"
def get_queryset(self):
return (
Claim.objects.filter(submitted_by=self.request.user)
.select_related("project")
.prefetch_related("logs__performed_by")
.order_by("-created_at")
)
class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateView):
template_name = "claims/user_management.html"
permission_required = "auth.view_user"
def _ensure_perm(self, perm_codename):
perm = f"auth.{perm_codename}"
if not self.request.user.has_perm(perm):
messages.error(self.request, _("Du saknar behörighet för åtgärden."))
return False
return True
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
users = User.objects.order_by("username")
rows = []
for user in users:
rows.append(
{
"user": user,
"permission_form": UserPermissionForm(
initial={
"user_id": user.id,
"is_staff": user.is_staff,
"grant_view": user.has_perm("claims.view_claim"),
"grant_change": user.has_perm("claims.change_claim"),
}
),
"delete_form": None
if user == self.request.user or user.is_superuser
else DeleteUserForm(initial={"user_id": user.id}),
}
)
context["user_rows"] = rows
context["create_form"] = kwargs.get("create_form") or UserManagementForm()
return context
def post(self, request, *args, **kwargs):
action = request.POST.get("action")
if action == "create":
if not self._ensure_perm("add_user"):
return redirect(request.path)
form = UserManagementForm(request.POST)
if form.is_valid():
user = User.objects.create_user(
username=form.cleaned_data["username"],
password=form.cleaned_data["password1"],
email=form.cleaned_data.get("email", ""),
first_name=form.cleaned_data.get("first_name", ""),
last_name=form.cleaned_data.get("last_name", ""),
is_staff=form.cleaned_data.get("is_staff", False),
)
self._set_perm(user, "claims.view_claim", form.cleaned_data.get("grant_view", False))
self._set_perm(user, "claims.change_claim", form.cleaned_data.get("grant_change", False))
messages.success(request, _("Användaren %(user)s skapades.") % {"user": user.username})
return redirect(request.path)
return self.render_to_response(self.get_context_data(create_form=form))
elif action == "update":
if not self._ensure_perm("change_user"):
return redirect(request.path)
form = UserPermissionForm(request.POST)
if form.is_valid():
user = get_object_or_404(User, pk=form.cleaned_data["user_id"])
if user == request.user and not form.cleaned_data["is_staff"]:
messages.error(request, _("Du kan inte ta bort din egen staff-status."))
return redirect(request.path)
user.is_staff = form.cleaned_data["is_staff"]
user.save(update_fields=["is_staff"])
self._set_perm(user, "claims.view_claim", form.cleaned_data["grant_view"])
self._set_perm(user, "claims.change_claim", form.cleaned_data["grant_change"])
messages.success(request, _("Behörigheter uppdaterades för %(user)s.") % {"user": user.username})
else:
messages.error(request, _("Kunde inte uppdatera behörigheter."))
return redirect(request.path)
elif action == "delete":
if not self._ensure_perm("delete_user"):
return redirect(request.path)
form = DeleteUserForm(request.POST)
if form.is_valid():
user = get_object_or_404(User, pk=form.cleaned_data["user_id"])
if user == request.user:
messages.error(request, _("Du kan inte ta bort ditt eget konto."))
elif user.is_superuser:
messages.error(request, _("Du kan inte ta bort en superuser via detta gränssnitt."))
else:
user.delete()
messages.warning(request, _("Användaren togs bort."))
return redirect(request.path)
messages.error(request, _("Okänd åtgärd."))
return redirect(request.path)
@staticmethod
def _set_perm(user, perm_label, should_have):
app_label, codename = perm_label.split(".")
perm = Permission.objects.filter(
content_type__app_label=app_label,
codename=codename,
).first()
if not perm:
return
if should_have:
user.user_permissions.add(perm)
else:
user.user_permissions.remove(perm)
class SubmitClaimSuccessView(TemplateView):
template_name = "claims/submit_success.html"