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.
- **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.

View File

@@ -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")}),

View File

@@ -258,11 +258,20 @@
{% endfor %}
</select>
<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>
<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>
<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">
<label class="block text-sm font-medium text-gray-700">{% trans "Evenemang/Projekt" %}</label>
<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" %}
</button>
</form>

View File

@@ -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)

View File

@@ -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,

Binary file not shown.

View File

@@ -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 <noreply@example.com>\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 ""