Allow approvers to adjust project before approving

This commit is contained in:
Victor Andersson
2025-11-09 13:57:54 +01:00
parent a953092718
commit 70aeca6187
7 changed files with 120 additions and 58 deletions

View File

@@ -26,7 +26,7 @@ Nyckel-URLer (språkprefixed):
- **Auto-prefill:** Inloggade användare får namn, e-post och senaste kontonummer förifyllt. - **Auto-prefill:** Inloggade användare får namn, e-post och senaste kontonummer förifyllt.
- **Valuta & projekt:** Varje rad har dold valutaväljare (SEK default) och projektreferens. Projekt listas från Django admin > Projekt. - **Valuta & projekt:** Varje rad har dold valutaväljare (SEK default) och projektreferens. Projekt listas från Django admin > Projekt.
- **Kvitton:** Filuppladdningar sparas med slumpat UUID-baserat namn under `receipts/` för säkerhet och unika namn. - **Kvitton:** Filuppladdningar sparas med slumpat UUID-baserat namn under `receipts/` för säkerhet och unika namn.
- **Dashboard:** KPI-kort med totalsiffror, senaste aktivitet, statusfördelning och samma inline-flöde för beslut/utbetalningar. - **Dashboard:** KPI-kort med totalsiffror, senaste aktivitet, statusfördelning och samma inline-flöde för beslut/utbetalningar (inkl. möjlighet att korrigera projekt vid beslut).
- **Betalspårning:** När intern betalning är på får godkända claims en "Betala"-knapp. När ett claim markeras som betalt låses status/kommentar tills reset görs. - **Betalspårning:** När intern betalning är på får godkända claims en "Betala"-knapp. När ett claim markeras som betalt låses status/kommentar tills reset görs.
- **Mina utlägg:** Inloggade ser sina egna claims i samma Tailwind-layout med kvitto-länk och logg. - **Mina utlägg:** Inloggade ser sina egna claims i samma Tailwind-layout med kvitto-länk och logg.
- **Användarhantering:** Tailwind-sida där personal kan skapa konton, tilldela `claims.view_claim`/`claims.change_claim`, markera staff och ta bort användare. - **Användarhantering:** Tailwind-sida där personal kan skapa konton, tilldela `claims.view_claim`/`claims.change_claim`, markera staff och ta bort användare.

View File

@@ -67,6 +67,15 @@ class ClaimDecisionForm(forms.Form):
claim_id = forms.IntegerField(widget=forms.HiddenInput) claim_id = forms.IntegerField(widget=forms.HiddenInput)
action = forms.ChoiceField(choices=ACTION_CHOICES) action = forms.ChoiceField(choices=ACTION_CHOICES)
project = forms.ModelChoiceField(
queryset=Project.objects.none(),
required=False,
label=_("Evenemang/Projekt"),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["project"].queryset = Project.objects.filter(is_active=True).order_by("name")
decision_note = forms.CharField( decision_note = forms.CharField(
required=False, required=False,
widget=forms.Textarea(attrs={"rows": 2, "placeholder": _("Kommentar")}), widget=forms.Textarea(attrs={"rows": 2, "placeholder": _("Kommentar")}),

View File

@@ -258,11 +258,20 @@
{% endfor %} {% endfor %}
</select> </select>
<label class="block text-sm font-medium text-gray-700">{% trans "Kommentar" %}</label> <label class="block text-sm font-medium text-gray-700">{% trans "Kommentar" %}</label>
<textarea name="decision_note" rows="3" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-600">{{ claim.decision_note }}</textarea> <textarea name="decision_note" rows="3" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-600">{{ claim.decision_note }}</textarea>
<input type="hidden" name="action_type" value="decision"> <label class="block text-sm font-medium text-gray-700">{% trans "Evenemang/Projekt" %}</label>
<button type="submit" class="w-full rounded-full bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700"> <select name="project" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-600">
<option value="">{% trans "Behåll nuvarande" %}</option>
{% for project in project_options %}
<option value="{{ project.id }}"{% if claim.project and project.id == claim.project.id %} selected{% endif %}>{{ project }}</option>
{% endfor %}
</select>
<p class="text-xs text-gray-500">{% trans "Justera projekt om underlaget skickats in mot fel evenemang." %}</p>
<input type="hidden" name="action_type" value="decision">
<button type="submit" class="w-full rounded-full bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700">
{% trans "Uppdatera beslut" %} {% trans "Uppdatera beslut" %}
</button> </button>
</form> </form>

View File

@@ -8,7 +8,8 @@ from django.test import TestCase, override_settings
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from .models import Claim from .forms import ClaimDecisionForm
from .models import Claim, Project
from .validators import validate_receipt_file from .validators import validate_receipt_file
from .views import SubmitClaimView from .views import SubmitClaimView
@@ -79,7 +80,8 @@ class DashboardViewTests(TestCase):
User = get_user_model() User = get_user_model()
self.user = User.objects.create_user(username="admin", password="test123", email="admin@example.com") self.user = User.objects.create_user(username="admin", password="test123", email="admin@example.com")
view_perm = Permission.objects.get(codename="view_claim") view_perm = Permission.objects.get(codename="view_claim")
self.user.user_permissions.add(view_perm) change_perm = Permission.objects.get(codename="change_claim")
self.user.user_permissions.add(view_perm, change_perm)
self.client.force_login(self.user) self.client.force_login(self.user)
def _create_claim(self, **kwargs): def _create_claim(self, **kwargs):
@@ -122,3 +124,23 @@ class DashboardViewTests(TestCase):
self._create_claim(status=Claim.Status.PENDING) self._create_claim(status=Claim.Status.PENDING)
response = self.client.get(reverse("claims:admin-list") + "?status=approved") response = self.client.get(reverse("claims:admin-list") + "?status=approved")
self.assertFalse(response.context["has_filtered_claims"]) self.assertFalse(response.context["has_filtered_claims"])
def test_attester_can_update_project_when_deciding(self):
project_old = Project.objects.create(name="Original", is_active=True)
project_new = Project.objects.create(name="Corrected", is_active=True)
claim = self._create_claim(project=project_old)
response = self.client.post(
reverse("claims:admin-list"),
{
"action_type": "decision",
"claim_id": claim.id,
"action": ClaimDecisionForm.ACTION_APPROVE,
"decision_note": "Updated project",
"project": project_new.id,
},
follow=True,
)
self.assertEqual(response.status_code, 200)
claim.refresh_from_db()
self.assertEqual(claim.project, project_new)

View File

@@ -24,7 +24,7 @@ from .forms import (
UserPermissionForm, UserPermissionForm,
) )
from .email_utils import notify_admin_of_claim, send_claimant_confirmation_email from .email_utils import notify_admin_of_claim, send_claimant_confirmation_email
from .models import Claim, ClaimLog from .models import Claim, ClaimLog, Project
User = get_user_model() User = get_user_model()
@@ -161,6 +161,7 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
context["can_change"] = self.request.user.has_perm("claims.change_claim") context["can_change"] = self.request.user.has_perm("claims.change_claim")
context["payments_enabled"] = getattr(settings, "CLAIMS_ENABLE_INTERNAL_PAYMENTS", False) context["payments_enabled"] = getattr(settings, "CLAIMS_ENABLE_INTERNAL_PAYMENTS", False)
context["summary"] = self._build_summary() context["summary"] = self._build_summary()
context["project_options"] = Project.objects.filter(is_active=True).order_by("name")
context["has_any_claims"] = context["summary"]["total_claims"] > 0 context["has_any_claims"] = context["summary"]["total_claims"] > 0
context["has_filtered_claims"] = self._has_filtered_claims(context["status_filter"], context["summary"]) context["has_filtered_claims"] = self._has_filtered_claims(context["status_filter"], context["summary"])
context["recent_claims"] = ( context["recent_claims"] = (
@@ -196,6 +197,11 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
return redirect(request.get_full_path()) return redirect(request.get_full_path())
previous_status = claim.status previous_status = claim.status
claim.decision_note = decision_note claim.decision_note = decision_note
new_project = form.cleaned_data.get("project")
project_changed = False
if new_project is not None and new_project != claim.project:
claim.project = new_project
project_changed = True
if action == ClaimDecisionForm.ACTION_APPROVE: if action == ClaimDecisionForm.ACTION_APPROVE:
claim.status = Claim.Status.APPROVED claim.status = Claim.Status.APPROVED
@@ -204,7 +210,10 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
claim.status = Claim.Status.REJECTED claim.status = Claim.Status.REJECTED
messages.warning(request, _("%(claim)s markerades som nekad.") % {"claim": claim}) messages.warning(request, _("%(claim)s markerades som nekad.") % {"claim": claim})
claim.save(update_fields=["status", "decision_note", "updated_at"]) update_fields = ["status", "decision_note", "updated_at"]
if project_changed:
update_fields.append("project")
claim.save(update_fields=update_fields)
claim.add_log( claim.add_log(
action=ClaimLog.Action.STATUS_CHANGED, action=ClaimLog.Action.STATUS_CHANGED,
performed_by=request.user, performed_by=request.user,

Binary file not shown.

View File

@@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: claims-system 0.1\n" "Project-Id-Version: claims-system 0.1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-09 13:13+0100\n" "POT-Creation-Date: 2025-11-09 13:48+0100\n"
"PO-Revision-Date: 2025-11-08 23:40+0100\n" "PO-Revision-Date: 2025-11-08 23:40+0100\n"
"Last-Translator: ChatGPT <noreply@example.com>\n" "Last-Translator: ChatGPT <noreply@example.com>\n"
"Language-Team: English\n" "Language-Team: English\n"
@@ -16,7 +16,7 @@ msgstr ""
msgid "Namn" msgid "Namn"
msgstr "Name" msgstr "Name"
#: claims/forms.py:23 claims/forms.py:86 #: claims/forms.py:23 claims/forms.py:95
#: claims/templates/claims/dashboard.html:169 #: claims/templates/claims/dashboard.html:169
msgid "E-post" msgid "E-post"
msgstr "Email" msgstr "Email"
@@ -38,7 +38,8 @@ msgstr "Amount"
msgid "Valuta" msgid "Valuta"
msgstr "Currency" msgstr "Currency"
#: claims/forms.py:52 #: claims/forms.py:52 claims/forms.py:73
#: claims/templates/claims/dashboard.html:264
msgid "Evenemang/Projekt" msgid "Evenemang/Projekt"
msgstr "Project" msgstr "Project"
@@ -54,76 +55,76 @@ msgstr "Approve"
msgid "Neka" msgid "Neka"
msgstr "Reject" msgstr "Reject"
#: claims/forms.py:72 claims/templates/claims/dashboard.html:126 #: claims/forms.py:81 claims/templates/claims/dashboard.html:126
#: claims/templates/claims/dashboard.html:261 #: claims/templates/claims/dashboard.html:261
msgid "Kommentar" msgid "Kommentar"
msgstr "Comment" msgstr "Comment"
#: claims/forms.py:80 #: claims/forms.py:89
msgid "Kommentar krävs när du nekar ett utlägg." msgid "Kommentar krävs när du nekar ett utlägg."
msgstr "A comment is required when you reject an expense." msgstr "A comment is required when you reject an expense."
#: claims/forms.py:85 #: claims/forms.py:94
msgid "Användarnamn" msgid "Användarnamn"
msgstr "Username" msgstr "Username"
#: claims/forms.py:87 #: claims/forms.py:96
msgid "Förnamn" msgid "Förnamn"
msgstr "First name" msgstr "First name"
#: claims/forms.py:88 #: claims/forms.py:97
msgid "Efternamn" msgid "Efternamn"
msgstr "Last name" msgstr "Last name"
#: claims/forms.py:89 #: claims/forms.py:98
msgid "Lösenord" msgid "Lösenord"
msgstr "Password" msgstr "Password"
#: claims/forms.py:90 #: claims/forms.py:99
msgid "Bekräfta lösenord" msgid "Bekräfta lösenord"
msgstr "Confirm password" msgstr "Confirm password"
#: claims/forms.py:91 #: claims/forms.py:100
msgid "Administratör (staff)" msgid "Administratör (staff)"
msgstr "Administrator (staff)" msgstr "Administrator (staff)"
#: claims/forms.py:92 #: claims/forms.py:101
msgid "Ge behörighet att se utlägg" msgid "Ge behörighet att se utlägg"
msgstr "Allow viewing claims" msgstr "Allow viewing claims"
#: claims/forms.py:93 #: claims/forms.py:102
msgid "Ge behörighet att besluta utlägg" msgid "Ge behörighet att besluta utlägg"
msgstr "Allow deciding claims" msgstr "Allow deciding claims"
#: claims/forms.py:98 #: claims/forms.py:107
msgid "Användarnamnet är upptaget." msgid "Användarnamnet är upptaget."
msgstr "That username is already taken." msgstr "That username is already taken."
#: claims/forms.py:104 #: claims/forms.py:113
msgid "Lösenorden matchar inte." msgid "Lösenorden matchar inte."
msgstr "Passwords do not match." msgstr "Passwords do not match."
#: claims/forms.py:122 claims/templates/claims/user_management.html:116 #: claims/forms.py:131 claims/templates/claims/user_management.html:116
msgid "Admin/staff" msgid "Admin/staff"
msgstr "Admin/staff" msgstr "Admin/staff"
#: claims/forms.py:123 claims/templates/claims/user_management.html:120 #: claims/forms.py:132 claims/templates/claims/user_management.html:120
msgid "Får se utlägg" msgid "Får se utlägg"
msgstr "May view claims" msgstr "May view claims"
#: claims/forms.py:124 claims/templates/claims/user_management.html:124 #: claims/forms.py:133 claims/templates/claims/user_management.html:124
msgid "Får besluta utlägg" msgid "Får besluta utlägg"
msgstr "May decide claims" msgstr "May decide claims"
#: claims/models.py:29 claims/templates/claims/dashboard.html:325 #: claims/models.py:29 claims/templates/claims/dashboard.html:334
msgid "Pending" msgid "Pending"
msgstr "Pending" msgstr "Pending"
#: claims/models.py:30 claims/templates/claims/dashboard.html:329 #: claims/models.py:30 claims/templates/claims/dashboard.html:338
msgid "Approved" msgid "Approved"
msgstr "Approved" msgstr "Approved"
#: claims/models.py:31 claims/templates/claims/dashboard.html:333 #: claims/models.py:31 claims/templates/claims/dashboard.html:342
msgid "Rejected" msgid "Rejected"
msgstr "Rejected" msgstr "Rejected"
@@ -241,7 +242,9 @@ msgstr "Track inflow, decisions, and payouts and act on claims immediately."
msgid "" msgid ""
"Tips: använd filtren för att fokusera på specifika statusar eller projekt. " "Tips: använd filtren för att fokusera på specifika statusar eller projekt. "
"Dashboarden uppdateras i realtid när data ändras." "Dashboarden uppdateras i realtid när data ändras."
msgstr "Tip: use the filters to focus on statuses or projects. The dashboard updates in real time." msgstr ""
"Tip: use the filters to focus on statuses or projects. The dashboard updates "
"in real time."
#: claims/templates/claims/dashboard.html:21 #: claims/templates/claims/dashboard.html:21
msgid "Totalt antal utlägg" msgid "Totalt antal utlägg"
@@ -366,7 +369,9 @@ msgstr "Project"
msgid "" msgid ""
"Använd referensen och beloppet när du lägger upp betalningen hjälper att " "Använd referensen och beloppet när du lägger upp betalningen hjälper att "
"undvika dubbletter." "undvika dubbletter."
msgstr "Use the reference and amount when entering the payment it helps avoid duplicates." msgstr ""
"Use the reference and amount when entering the payment it helps avoid "
"duplicates."
#: claims/templates/claims/dashboard.html:182 #: claims/templates/claims/dashboard.html:182
msgid "" msgid ""
@@ -434,43 +439,51 @@ msgid "Åtgärd"
msgstr "Action" msgstr "Action"
#: claims/templates/claims/dashboard.html:266 #: claims/templates/claims/dashboard.html:266
msgid "Behåll nuvarande"
msgstr "Keep current"
#: claims/templates/claims/dashboard.html:271
msgid "Justera projekt om underlaget skickats in mot fel evenemang."
msgstr "Adjust the project if the submission was sent against the wrong event."
#: claims/templates/claims/dashboard.html:275
msgid "Uppdatera beslut" msgid "Uppdatera beslut"
msgstr "Update decision" msgstr "Update decision"
#: claims/templates/claims/dashboard.html:277 #: claims/templates/claims/dashboard.html:286
#: claims/templates/claims/my_claims.html:78 #: claims/templates/claims/my_claims.html:78
msgid "Inga utlägg ännu" msgid "Inga utlägg ännu"
msgstr "No claims yet" msgstr "No claims yet"
#: claims/templates/claims/dashboard.html:278 #: claims/templates/claims/dashboard.html:287
msgid "När formuläret tas emot visas posterna automatiskt här." msgid "När formuläret tas emot visas posterna automatiskt här."
msgstr "As soon as submissions arrive they will appear here." msgstr "As soon as submissions arrive they will appear here."
#: claims/templates/claims/dashboard.html:284 #: claims/templates/claims/dashboard.html:293
msgid "Inga utlägg matchar filtret" msgid "Inga utlägg matchar filtret"
msgstr "No claims match the filter" msgstr "No claims match the filter"
#: claims/templates/claims/dashboard.html:285 #: claims/templates/claims/dashboard.html:294
msgid "Välj en annan status för att se fler poster." msgid "Välj en annan status för att se fler poster."
msgstr "Choose another status to see more entries." msgstr "Choose another status to see more entries."
#: claims/templates/claims/dashboard.html:293 #: claims/templates/claims/dashboard.html:302
msgid "Senaste inskick" msgid "Senaste inskick"
msgstr "Latest submissions" msgstr "Latest submissions"
#: claims/templates/claims/dashboard.html:294 #: claims/templates/claims/dashboard.html:303
msgid "Aktivitet" msgid "Aktivitet"
msgstr "Activity" msgstr "Activity"
#: claims/templates/claims/dashboard.html:312 #: claims/templates/claims/dashboard.html:321
msgid "Inga aktiviteter än." msgid "Inga aktiviteter än."
msgstr "No activity yet." msgstr "No activity yet."
#: claims/templates/claims/dashboard.html:320 #: claims/templates/claims/dashboard.html:329
msgid "Statusfördelning" msgid "Statusfördelning"
msgstr "Status breakdown" msgstr "Status breakdown"
#: claims/templates/claims/dashboard.html:321 #: claims/templates/claims/dashboard.html:330
msgid "Snabbstatistik" msgid "Snabbstatistik"
msgstr "Quick stats" msgstr "Quick stats"
@@ -827,87 +840,87 @@ msgstr ""
msgid "Kunde inte spara utläggen. Kontrollera formuläret." msgid "Kunde inte spara utläggen. Kontrollera formuläret."
msgstr "" msgstr ""
#: claims/views.py:181 claims/views.py:222 #: claims/views.py:182 claims/views.py:231
#, fuzzy #, fuzzy
#| msgid "Ge behörighet att besluta utlägg" #| msgid "Ge behörighet att besluta utlägg"
msgid "Du har inte behörighet att uppdatera utlägg." msgid "Du har inte behörighet att uppdatera utlägg."
msgstr "Allow deciding claims" msgstr "Allow deciding claims"
#: claims/views.py:195 #: claims/views.py:196
#, fuzzy #, fuzzy
#| msgid "" #| msgid ""
#| "Utlägget är markerat som betalt. Ändringar av beslut/kommentar är låsta." #| "Utlägget är markerat som betalt. Ändringar av beslut/kommentar är låsta."
msgid "Utlägget är redan markerat som betalt och kan inte ändras." msgid "Utlägget är redan markerat som betalt och kan inte ändras."
msgstr "The claim is marked as paid. Decision/comments are locked." msgstr "The claim is marked as paid. Decision/comments are locked."
#: claims/views.py:202 #: claims/views.py:208
#, python-format #, python-format
msgid "%(claim)s markerades som godkänd." msgid "%(claim)s markerades som godkänd."
msgstr "" msgstr ""
#: claims/views.py:205 #: claims/views.py:211
#, fuzzy, python-format #, fuzzy, python-format
#| msgid "Ej markerad som betald" #| msgid "Ej markerad som betald"
msgid "%(claim)s markerades som nekad." msgid "%(claim)s markerades som nekad."
msgstr "Not marked as paid" msgstr "Not marked as paid"
#: claims/views.py:219 #: claims/views.py:228
msgid "Betalningshantering är inte aktiverad." msgid "Betalningshantering är inte aktiverad."
msgstr "" msgstr ""
#: claims/views.py:227 #: claims/views.py:236
msgid "Endast godkända utlägg kan markeras som betalda." msgid "Endast godkända utlägg kan markeras som betalda."
msgstr "" msgstr ""
#: claims/views.py:230 #: claims/views.py:239
msgid "Detta utlägg är redan markerat som betalt." msgid "Detta utlägg är redan markerat som betalt."
msgstr "" msgstr ""
#: claims/views.py:241 #: claims/views.py:250
#, fuzzy, python-format #, fuzzy, python-format
#| msgid "Ej markerad som betald" #| msgid "Ej markerad som betald"
msgid "%(claim)s markerades som betald." msgid "%(claim)s markerades som betald."
msgstr "Not marked as paid" msgstr "Not marked as paid"
#: claims/views.py:307 #: claims/views.py:316
msgid "Du saknar behörighet för åtgärden." msgid "Du saknar behörighet för åtgärden."
msgstr "" msgstr ""
#: claims/views.py:354 #: claims/views.py:363
#, python-format #, python-format
msgid "Användaren %(user)s skapades." msgid "Användaren %(user)s skapades."
msgstr "" msgstr ""
#: claims/views.py:365 #: claims/views.py:374
msgid "Du kan inte ta bort din egen staff-status." msgid "Du kan inte ta bort din egen staff-status."
msgstr "" msgstr ""
#: claims/views.py:371 #: claims/views.py:380
#, python-format #, python-format
msgid "Behörigheter uppdaterades för %(user)s." msgid "Behörigheter uppdaterades för %(user)s."
msgstr "" msgstr ""
#: claims/views.py:373 #: claims/views.py:382
#, fuzzy #, fuzzy
#| msgid "Justera behörigheter" #| msgid "Justera behörigheter"
msgid "Kunde inte uppdatera behörigheter." msgid "Kunde inte uppdatera behörigheter."
msgstr "Adjust permissions" msgstr "Adjust permissions"
#: claims/views.py:383 #: claims/views.py:392
msgid "Du kan inte ta bort ditt eget konto." msgid "Du kan inte ta bort ditt eget konto."
msgstr "" msgstr ""
#: claims/views.py:385 #: claims/views.py:394
msgid "Du kan inte ta bort en superuser via detta gränssnitt." msgid "Du kan inte ta bort en superuser via detta gränssnitt."
msgstr "" msgstr ""
#: claims/views.py:388 #: claims/views.py:397
#, fuzzy #, fuzzy
#| msgid "Användare" #| msgid "Användare"
msgid "Användaren togs bort." msgid "Användaren togs bort."
msgstr "Users" msgstr "Users"
#: claims/views.py:391 #: claims/views.py:400
msgid "Okänd åtgärd." msgid "Okänd åtgärd."
msgstr "" msgstr ""