Allow approvers to adjust project before approving
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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")}),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
@@ -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 ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user