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"