From 70aeca618705432017e7a2900d29d676b542b842 Mon Sep 17 00:00:00 2001 From: Victor Andersson Date: Sun, 9 Nov 2025 13:57:54 +0100 Subject: [PATCH] Allow approvers to adjust project before approving --- README.md | 2 +- claims/forms.py | 9 ++ claims/templates/claims/dashboard.html | 17 +++- claims/tests.py | 26 +++++- claims/views.py | 13 ++- locale/en/LC_MESSAGES/django.mo | Bin 14173 -> 14368 bytes locale/en/LC_MESSAGES/django.po | 111 ++++++++++++++----------- 7 files changed, 120 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 03fe0c9..b6df897 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Nyckel-URLer (språkprefixed): - **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. - **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. - **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. diff --git a/claims/forms.py b/claims/forms.py index 283fa99..b71b1bf 100644 --- a/claims/forms.py +++ b/claims/forms.py @@ -67,6 +67,15 @@ class ClaimDecisionForm(forms.Form): claim_id = forms.IntegerField(widget=forms.HiddenInput) 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( required=False, widget=forms.Textarea(attrs={"rows": 2, "placeholder": _("Kommentar")}), diff --git a/claims/templates/claims/dashboard.html b/claims/templates/claims/dashboard.html index 6233333..dd738b2 100644 --- a/claims/templates/claims/dashboard.html +++ b/claims/templates/claims/dashboard.html @@ -258,11 +258,20 @@ {% endfor %} - - + + - - diff --git a/claims/tests.py b/claims/tests.py index bbd0be9..7fc67ac 100644 --- a/claims/tests.py +++ b/claims/tests.py @@ -8,7 +8,8 @@ from django.test import TestCase, override_settings from django.urls import reverse 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 .views import SubmitClaimView @@ -79,7 +80,8 @@ class DashboardViewTests(TestCase): User = get_user_model() self.user = User.objects.create_user(username="admin", password="test123", email="admin@example.com") 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) def _create_claim(self, **kwargs): @@ -122,3 +124,23 @@ class DashboardViewTests(TestCase): self._create_claim(status=Claim.Status.PENDING) response = self.client.get(reverse("claims:admin-list") + "?status=approved") 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) diff --git a/claims/views.py b/claims/views.py index d154e42..e556eca 100644 --- a/claims/views.py +++ b/claims/views.py @@ -24,7 +24,7 @@ from .forms import ( UserPermissionForm, ) 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() @@ -161,6 +161,7 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView): context["can_change"] = self.request.user.has_perm("claims.change_claim") context["payments_enabled"] = getattr(settings, "CLAIMS_ENABLE_INTERNAL_PAYMENTS", False) 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_filtered_claims"] = self._has_filtered_claims(context["status_filter"], context["summary"]) context["recent_claims"] = ( @@ -196,6 +197,11 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView): return redirect(request.get_full_path()) previous_status = claim.status 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: claim.status = Claim.Status.APPROVED @@ -204,7 +210,10 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView): claim.status = Claim.Status.REJECTED 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( action=ClaimLog.Action.STATUS_CHANGED, performed_by=request.user, diff --git a/locale/en/LC_MESSAGES/django.mo b/locale/en/LC_MESSAGES/django.mo index d97c1c81486767b7bab5637f7aadc8cc1ab8b6f5..1f2c5a61bed6f9c98d4f6b2e568dc84280c95d9f 100644 GIT binary patch delta 4025 zcmYM$c~F#P0LSqMQBVX_)DSIv4MDsRZ!Hr<^Nd0>BQtD;WnFIo}sg1e%w}e#q!} zzoEQDE+>aO8FLONb>)L{A>EjscoB6ft(!5IVHS45LhOiFVHy@;5tdr-!77eF!xS9V z-M#J_yuz56nM0*BU-+>DhA6LScdA@PEdZ=!7&_wZJ3NnOrtuSj%ufOjEcS(YEO)| z&O%M$V$_Jjs1dJ5?UBvahfzy#7`4W4U;%!HA^aEBV33V42pf>eGy72Yi5;b)hCV`# z=me@mzuMzJ?e~f7MBOOUnvJ?X7j>gC_WX2fDQc?c<6x{n&BSJV{u!j*m}#X_%@-e{ zHpvKfpk5>;sI@Lbt-TM`P#w<47-}TPaV?&~$ryCp&G4R1ho{65t04q_r6 zL2c$%)YEkg)uD?R(+KkN+$mm-`e7q7o2JPgKZ_kWZbfaPqo}q25q0CU_zzyP=l|v+ zJq4Ze-Kj6eB#!5zmaH5#Q}y}GzaFo%==D!n_d7S8i<){mm-~xVW675HA!k1%Iv^zgW-ROH%Lzhq^NMxb3 zH@c%5%tl>*wLO0wYQ`4Y<0y9KcpYj+w^^G}4Ys0Y@FPsc*mqPk;$JX`7f=^09qyiA zfzvtOilgyMR0lg1*f%HYxCr&Q&cJl6u;=Sgn=opRSE4$&7TF^)vz5xNoOsWEF<=BQ z8IFhGSS-PGyu-Q)wN!ghd#4$-BnNOM9>#8%$A;*Na16eHJi(?7v-SKZjdVwl zi@MQp?2cpX`D;-PmSPXQ88wxD)JWE#W?&Ph;&xQSdr>oc$R59rjM036x8UEHO#7y6 z6t7V%#~!!_({Kl>p?#TVXIF|WWrI-_HpbXW(BGia#P_O0)vRTXyRKuU3Zq$al?sr^_|Jd^j#~Cw|V;^dy zM^M+lhPwU(Xo#T$^xRl z49}Bih<1k_QkCT{@z;;GqsmZn9VyfS%w6~ zB@|zfD78go6DcEMl1EhL5uUpEe^7LsPKwArvX&en-H3{pOpDiyc@kII`U-11#qAZ` zt{(TJvWMuIy`QLDNhXjnWIE|g9w*C46?uZ}Ap1#sSx;p$@snK%xx-pIcH5nhdZ4z} z2}Bz_Ay1&HWoP$0lPArNh9lmP$5|cO_`$-P=}1 u9DjBEFdSV{=MRVd!GN>E6L!M<#PRq%{(!xFMJO2X#aoCJw4@C$OZg8#VxIv3 delta 3834 zcmYk<2TWC00LJkHq7RWJASiV)90;hmz&&vm2So+B&{TBWs#CMFtf6IV@}{=at*jW_+?@0|1AJL{hNu>11w=DBM^e6|{{ zlcXuxRo9p&DmUPT>z6=dLh%-==`n`mE3A*fLB{xCBnDs$?2C!kT%1My82VvIu=BZQ z*vuHWiKpPp8^h2W)3Gg%#YVUaJ9!yXj`U$JHZ-OW-b76-Jj9rK*be#6B=Mq&4zlf| zQJ))+EpfhW-;DJc-;`0%igqJon6I%lUO-Lk5q868Sbz!i#^TLB^v4r82G5};7#LY$K93oJsuYYt!&Jc~LLH&9>r3$^095zfFN zsEI_QzL$(_f=NStZaV6Fd6qd_x%hU(Zm($R&* z)csLkC`X;*N}Pkou?I%HC0|otXTu0sOd#HQ= z5;f4jn1R8ZP^}~z*WfDbgQlf(2>YUTCIhuIv#f=vL%JTd(4EL8n@UX6^M9U#?o9yG z)kI=YD^J2wOhHZL5{|{&s9V&LXHB=LJ8Ffgs9P}#6L1wWcXJfA^2_LrHK=j!p^Nd& z6AHQb0@ZPLj4>lH57qH^*xSpPYE(zwt(<|wQ4^0weJ=&;V1LwMPDMRk>8J^9LM`B1 ztcwAy*?)a8lmfeHnxg8x&>K@xhiEA3u;k<4=(hD`ys6W_5w+#jSPyTYZq*&s4n0FX zRbFkK1=Pbx>XB{Pe|@1V4O(e0)K(8h4Va0HWpdDkTd*T;!%V!0T4~ET$3z@Xy$9-t zrwDuFcGQZ0#`<^%_5Oo6_J16OS2XCk9^K9vXd-H2Gm$6C%tP%^v28zuL#Q7|P53$L z@cG6&{WM11ni$mK>yFx?k*I#Aq88+KQ_u>EQHP=&HQ?u{FPyjS*HBwlW9zTbm%57^ zt1}U7jYSQZg4)3}^v6l470<-k(Yw9VJ_Y&tH#yjq@r|28APwJGt5NspI_l8eMcs-&a49}S zZS~9!&fzM+1=QWx1s@?#sEOjB=II;k+~Tkwk-=gqrO*yJaJ|#HpH_SfLAe$@y)Lk)bUf)ME*ey z;OgYeG!!+FSkwvdpw0J6f|Hg2S6Qn zL_NQKQ7gzq&3GF2$2`k4+E(>9i)ez0L<65BEy-Te zhP+MEh{iYTi0+eC5=?X*BOAzMGMgMC9}!*i$q`S9#}M`S>8c>XCJfcenLx-i9yi0WHP=t_5GMMO>W-HOn z$rV)f67tghy*F8iJ`gBhV z|A&G4#d-4@_eSLl%6^RR`KPGbm)kc#!Ejq%h{MQA(vvhIYseJRf`pM1MAs4r&##I< z<;~`Ug21ZRtxM}w{ggP$?>}S?Tj&4) diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index f373ee4..30d2986 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: claims-system 0.1\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" "Last-Translator: ChatGPT \n" "Language-Team: English\n" @@ -16,7 +16,7 @@ msgstr "" msgid "Namn" msgstr "Name" -#: claims/forms.py:23 claims/forms.py:86 +#: claims/forms.py:23 claims/forms.py:95 #: claims/templates/claims/dashboard.html:169 msgid "E-post" msgstr "Email" @@ -38,7 +38,8 @@ msgstr "Amount" msgid "Valuta" msgstr "Currency" -#: claims/forms.py:52 +#: claims/forms.py:52 claims/forms.py:73 +#: claims/templates/claims/dashboard.html:264 msgid "Evenemang/Projekt" msgstr "Project" @@ -54,76 +55,76 @@ msgstr "Approve" msgid "Neka" 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 msgid "Kommentar" msgstr "Comment" -#: claims/forms.py:80 +#: claims/forms.py:89 msgid "Kommentar krävs när du nekar ett utlägg." msgstr "A comment is required when you reject an expense." -#: claims/forms.py:85 +#: claims/forms.py:94 msgid "Användarnamn" msgstr "Username" -#: claims/forms.py:87 +#: claims/forms.py:96 msgid "Förnamn" msgstr "First name" -#: claims/forms.py:88 +#: claims/forms.py:97 msgid "Efternamn" msgstr "Last name" -#: claims/forms.py:89 +#: claims/forms.py:98 msgid "Lösenord" msgstr "Password" -#: claims/forms.py:90 +#: claims/forms.py:99 msgid "Bekräfta lösenord" msgstr "Confirm password" -#: claims/forms.py:91 +#: claims/forms.py:100 msgid "Administratör (staff)" msgstr "Administrator (staff)" -#: claims/forms.py:92 +#: claims/forms.py:101 msgid "Ge behörighet att se utlägg" msgstr "Allow viewing claims" -#: claims/forms.py:93 +#: claims/forms.py:102 msgid "Ge behörighet att besluta utlägg" msgstr "Allow deciding claims" -#: claims/forms.py:98 +#: claims/forms.py:107 msgid "Användarnamnet är upptaget." msgstr "That username is already taken." -#: claims/forms.py:104 +#: claims/forms.py:113 msgid "Lösenorden matchar inte." 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" 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" 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" 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" msgstr "Pending" -#: claims/models.py:30 claims/templates/claims/dashboard.html:329 +#: claims/models.py:30 claims/templates/claims/dashboard.html:338 msgid "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" msgstr "Rejected" @@ -241,7 +242,9 @@ msgstr "Track inflow, decisions, and payouts – and act on claims immediately." msgid "" "Tips: använd filtren för att fokusera på specifika statusar eller projekt. " "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 msgid "Totalt antal utlägg" @@ -366,7 +369,9 @@ msgstr "Project" msgid "" "Använd referensen och beloppet när du lägger upp betalningen – hjälper att " "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 msgid "" @@ -434,43 +439,51 @@ msgid "Åtgärd" msgstr "Action" #: 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" msgstr "Update decision" -#: claims/templates/claims/dashboard.html:277 +#: claims/templates/claims/dashboard.html:286 #: claims/templates/claims/my_claims.html:78 msgid "Inga utlägg ännu" 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." 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" 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." msgstr "Choose another status to see more entries." -#: claims/templates/claims/dashboard.html:293 +#: claims/templates/claims/dashboard.html:302 msgid "Senaste inskick" msgstr "Latest submissions" -#: claims/templates/claims/dashboard.html:294 +#: claims/templates/claims/dashboard.html:303 msgid "Aktivitet" msgstr "Activity" -#: claims/templates/claims/dashboard.html:312 +#: claims/templates/claims/dashboard.html:321 msgid "Inga aktiviteter än." msgstr "No activity yet." -#: claims/templates/claims/dashboard.html:320 +#: claims/templates/claims/dashboard.html:329 msgid "Statusfördelning" msgstr "Status breakdown" -#: claims/templates/claims/dashboard.html:321 +#: claims/templates/claims/dashboard.html:330 msgid "Snabbstatistik" msgstr "Quick stats" @@ -827,87 +840,87 @@ msgstr "" msgid "Kunde inte spara utläggen. Kontrollera formuläret." msgstr "" -#: claims/views.py:181 claims/views.py:222 +#: claims/views.py:182 claims/views.py:231 #, fuzzy #| msgid "Ge behörighet att besluta utlägg" msgid "Du har inte behörighet att uppdatera utlägg." msgstr "Allow deciding claims" -#: claims/views.py:195 +#: claims/views.py:196 #, fuzzy #| msgid "" #| "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." msgstr "The claim is marked as paid. Decision/comments are locked." -#: claims/views.py:202 +#: claims/views.py:208 #, python-format msgid "%(claim)s markerades som godkänd." msgstr "" -#: claims/views.py:205 +#: claims/views.py:211 #, fuzzy, python-format #| msgid "Ej markerad som betald" msgid "%(claim)s markerades som nekad." msgstr "Not marked as paid" -#: claims/views.py:219 +#: claims/views.py:228 msgid "Betalningshantering är inte aktiverad." msgstr "" -#: claims/views.py:227 +#: claims/views.py:236 msgid "Endast godkända utlägg kan markeras som betalda." msgstr "" -#: claims/views.py:230 +#: claims/views.py:239 msgid "Detta utlägg är redan markerat som betalt." msgstr "" -#: claims/views.py:241 +#: claims/views.py:250 #, fuzzy, python-format #| msgid "Ej markerad som betald" msgid "%(claim)s markerades som betald." msgstr "Not marked as paid" -#: claims/views.py:307 +#: claims/views.py:316 msgid "Du saknar behörighet för åtgärden." msgstr "" -#: claims/views.py:354 +#: claims/views.py:363 #, python-format msgid "Användaren %(user)s skapades." msgstr "" -#: claims/views.py:365 +#: claims/views.py:374 msgid "Du kan inte ta bort din egen staff-status." msgstr "" -#: claims/views.py:371 +#: claims/views.py:380 #, python-format msgid "Behörigheter uppdaterades för %(user)s." msgstr "" -#: claims/views.py:373 +#: claims/views.py:382 #, fuzzy #| msgid "Justera behörigheter" msgid "Kunde inte uppdatera behörigheter." msgstr "Adjust permissions" -#: claims/views.py:383 +#: claims/views.py:392 msgid "Du kan inte ta bort ditt eget konto." msgstr "" -#: claims/views.py:385 +#: claims/views.py:394 msgid "Du kan inte ta bort en superuser via detta gränssnitt." msgstr "" -#: claims/views.py:388 +#: claims/views.py:397 #, fuzzy #| msgid "Användare" msgid "Användaren togs bort." msgstr "Users" -#: claims/views.py:391 +#: claims/views.py:400 msgid "Okänd åtgärd." msgstr ""