Compare commits

...

10 Commits

Author SHA1 Message Date
Victor Andersson
559ed671f3 Attach edit button handler with JS listeners 2025-11-10 23:20:59 +01:00
Victor Andersson
9499eb6395 Render edit overlay per claim 2025-11-09 22:18:22 +01:00
Victor Andersson
968150b074 Fix edit button for text-node targets 2025-11-09 22:05:01 +01:00
Victor Andersson
c189fd053c Expose edit panel opener globally 2025-11-09 22:03:29 +01:00
Victor Andersson
3323ffd82e Use delegated handlers for edit panel 2025-11-09 22:00:14 +01:00
Victor Andersson
f42381a9a0 Make edit panel work without dialog support 2025-11-09 21:58:04 +01:00
Victor Andersson
78377a7ae9 Switch edit overlay to modal dialog 2025-11-09 21:53:22 +01:00
Victor Andersson
caf3df24cf Add inline edit panel for claims 2025-11-09 21:49:44 +01:00
Victor Andersson
0d68c75fef Log project changes during approvals 2025-11-09 14:05:28 +01:00
Victor Andersson
70aeca6187 Allow approvers to adjust project before approving 2025-11-09 13:57:54 +01:00
8 changed files with 534 additions and 181 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. Attestanter kan öppna en redigeringspanel för att justera namn, belopp, valuta, kontonummer och projekt innan 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,11 +67,21 @@ 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"),
)
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")}),
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["project"].queryset = Project.objects.filter(is_active=True).order_by("name")
def clean(self): def clean(self):
cleaned = super().clean() cleaned = super().clean()
action = cleaned.get("action") action = cleaned.get("action")
@@ -81,6 +91,29 @@ class ClaimDecisionForm(forms.Form):
return cleaned return cleaned
class ClaimEditForm(forms.ModelForm):
class Meta:
model = Claim
fields = [
"full_name",
"email",
"account_number",
"amount",
"currency",
"project",
"description",
]
labels = {
"full_name": _("Namn"),
"email": _("E-post"),
"account_number": _("Kontonummer"),
"amount": _("Belopp"),
"currency": _("Valuta"),
"project": _("Evenemang/Projekt"),
"description": _("Beskrivning"),
}
class UserManagementForm(forms.Form): class UserManagementForm(forms.Form):
username = forms.CharField(max_length=150, label=_("Användarnamn")) username = forms.CharField(max_length=150, label=_("Användarnamn"))
email = forms.EmailField(required=False, label=_("E-post")) email = forms.EmailField(required=False, label=_("E-post"))

View File

@@ -122,6 +122,8 @@ class ClaimLog(models.Model):
CREATED = "created", _("Submitted") CREATED = "created", _("Submitted")
STATUS_CHANGED = "status_changed", _("Status changed") STATUS_CHANGED = "status_changed", _("Status changed")
MARKED_PAID = "marked_paid", _("Marked as paid") MARKED_PAID = "marked_paid", _("Marked as paid")
PROJECT_CHANGED = "project_changed", _("Project changed")
DETAILS_EDITED = "details_edited", _("Details edited")
claim = models.ForeignKey(Claim, related_name="logs", on_delete=models.CASCADE) claim = models.ForeignKey(Claim, related_name="logs", on_delete=models.CASCADE)
action = models.CharField(max_length=32, choices=Action.choices) action = models.CharField(max_length=32, choices=Action.choices)
@@ -147,4 +149,3 @@ class ClaimLog(models.Model):
def __str__(self): def __str__(self):
return f"{self.get_action_display()} ({self.created_at:%Y-%m-%d %H:%M})" return f"{self.get_action_display()} ({self.created_at:%Y-%m-%d %H:%M})"

View File

@@ -85,6 +85,82 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% if can_change %}
<div class="fixed inset-0 z-40 hidden items-center justify-center bg-slate-900/80 p-4"
data-edit-panel="{{ claim.id }}"
data-edit-backdrop="{{ claim.id }}"
aria-hidden="true"
role="dialog"
aria-modal="true">
<div class="w-full max-w-2xl rounded-3xl bg-white p-6 text-left shadow-2xl">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">{% trans "Redigera utlägg" %}</p>
<h3 class="text-xl font-semibold text-gray-900">{{ claim.full_name }}</h3>
</div>
<button type="button"
class="rounded-full bg-gray-100 px-3 py-1 text-xs font-semibold text-gray-600 transition hover:bg-gray-200"
onclick="claimsCloseEdit('{{ claim.id }}'); return false;">
{% trans "Stäng" %}
</button>
</div>
<form method="post" class="mt-4 space-y-4">
{% csrf_token %}
<input type="hidden" name="action_type" value="edit">
<input type="hidden" name="edit_claim_id" value="{{ claim.id }}">
<div class="grid gap-4 md:grid-cols-2">
<label class="text-sm font-medium text-gray-700">
{% trans "Namn" %}
<input type="text" name="full_name" value="{{ claim.full_name }}" class="mt-1 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" required>
</label>
<label class="text-sm font-medium text-gray-700">
{% trans "E-post" %}
<input type="email" name="email" value="{{ claim.email }}" class="mt-1 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" required>
</label>
<label class="text-sm font-medium text-gray-700">
{% trans "Kontonummer" %}
<input type="text" name="account_number" value="{{ claim.account_number }}" class="mt-1 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" required>
</label>
<label class="text-sm font-medium text-gray-700">
{% trans "Belopp" %}
<input type="number" step="0.01" name="amount" value="{{ claim.amount }}" class="mt-1 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" required>
</label>
<label class="text-sm font-medium text-gray-700">
{% trans "Valuta" %}
<select name="currency" class="mt-1 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">
{% for value, label in currency_choices %}
<option value="{{ value }}"{% if claim.currency == value %} selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label class="text-sm font-medium text-gray-700">
{% trans "Evenemang/Projekt" %}
<select name="project" class="mt-1 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 "Ingen" %}</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>
</label>
</div>
<div>
<label class="text-sm font-medium text-gray-700" for="edit-description-{{ claim.id }}">{% trans "Beskrivning" %}</label>
<textarea id="edit-description-{{ claim.id }}" name="description" rows="4" class="mt-1 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.description }}</textarea>
</div>
<div class="flex items-center justify-end gap-3">
<button type="button"
class="rounded-full border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-600 transition hover:bg-gray-100"
onclick="claimsCloseEdit('{{ claim.id }}'); return false;">
{% trans "Avbryt" %}
</button>
<button type="submit" class="rounded-full bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700">
{% trans "Spara ändringar" %}
</button>
</div>
</form>
</div>
</div>
{% endif %}
</section> </section>
<div class="space-y-6" data-claim-list> <div class="space-y-6" data-claim-list>
@@ -118,25 +194,32 @@
</p> </p>
</div> </div>
</div> </div>
<div class="flex flex-col items-start gap-2 text-sm lg:items-end"> <div class="flex flex-col items-start gap-2 text-sm lg:items-end">
<span class="rounded-full px-4 py-2 text-sm font-semibold {% if claim.status == 'approved' %}bg-green-50 text-green-700 border border-green-200{% elif claim.status == 'rejected' %}bg-rose-50 text-rose-700 border border-rose-200{% else %}bg-amber-50 text-amber-800 border border-amber-200{% endif %}"> <span class="rounded-full px-4 py-2 text-sm font-semibold {% if claim.status == 'approved' %}bg-green-50 text-green-700 border border-green-200{% elif claim.status == 'rejected' %}bg-rose-50 text-rose-700 border border-rose-200{% else %}bg-amber-50 text-amber-800 border border-amber-200{% endif %}">
{{ claim.get_status_display }} {{ claim.get_status_display }}
</span> </span>
{% if claim.decision_note %} {% if claim.decision_note %}
<p class="text-xs text-gray-500">{% trans "Kommentar" %}: {{ claim.decision_note }}</p> <p class="text-xs text-gray-500">{% trans "Kommentar" %}: {{ claim.decision_note }}</p>
{% endif %} {% endif %}
{% if payments_enabled and claim.status == 'approved' %} {% if payments_enabled and claim.status == 'approved' %}
{% if claim.is_paid %} {% if claim.is_paid %}
<span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-800"> <span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-800">
{% trans "Betald" %} {{ claim.paid_at|date:"Y-m-d H:i" }} {% trans "Betald" %} {{ claim.paid_at|date:"Y-m-d H:i" }}
{% if claim.paid_by %}{% trans "av" %} {{ claim.paid_by.get_username }}{% endif %} {% if claim.paid_by %}{% trans "av" %} {{ claim.paid_by.get_username }}{% endif %}
</span> </span>
{% else %} {% else %}
<span class="text-xs text-gray-500">{% trans "Ej markerad som betald" %}</span> <span class="text-xs text-gray-500">{% trans "Ej markerad som betald" %}</span>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> {% if can_change %}
</div> <button type="button"
data-open-edit="{{ claim.id }}"
class="rounded-full border border-gray-300 px-3 py-1 text-xs font-semibold text-gray-700 transition hover:bg-gray-100">
{% trans "Redigera" %}
</button>
{% endif %}
</div>
</div>
{% if claim.status == 'approved' %} {% if claim.status == 'approved' %}
<div class="mx-6 mt-4 grid gap-4 rounded-3xl border border-green-100 bg-green-50 px-6 py-4 text-sm text-green-900 {% if payments_enabled and claim.is_paid %}md:grid-cols-1{% else %}md:grid-cols-[2fr,1fr]{% endif %}"> <div class="mx-6 mt-4 grid gap-4 rounded-3xl border border-green-100 bg-green-50 px-6 py-4 text-sm text-green-900 {% if payments_enabled and claim.is_paid %}md:grid-cols-1{% else %}md:grid-cols-[2fr,1fr]{% endif %}">
@@ -258,11 +341,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>
@@ -339,73 +431,125 @@
</div> </div>
</section> </section>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { (function () {
const filterButtons = Array.from(document.querySelectorAll("[data-filter-button]")); function openPanel(id) {
const cards = Array.from(document.querySelectorAll("[data-claim-card]")); const panel = document.querySelector(`[data-edit-panel="${id}"]`);
const emptyState = document.querySelector("[data-claim-empty]"); if (!panel) return;
if (!filterButtons.length || !cards.length) { panel.classList.remove("hidden");
return; panel.classList.add("flex");
panel.setAttribute("aria-hidden", "false");
} }
const activeClasses = ["bg-brand-600", "text-white", "hover:bg-brand-700"]; function closePanelElement(panel) {
const inactiveClasses = ["bg-slate-100", "text-gray-700", "hover:bg-slate-200"]; panel.classList.add("hidden");
panel.classList.remove("flex");
panel.setAttribute("aria-hidden", "true");
}
const setButtonState = (activeValue) => { document.addEventListener("click", (event) => {
filterButtons.forEach((btn) => { const backdrop = event.target.closest("[data-edit-backdrop]");
const value = btn.dataset.filterValue || "all"; if (backdrop && event.target === backdrop) {
const isActive = value === activeValue; closePanelElement(backdrop);
btn.setAttribute("aria-pressed", String(isActive));
const classList = btn.classList;
if (isActive) {
inactiveClasses.forEach((cls) => classList.remove(cls));
activeClasses.forEach((cls) => classList.add(cls));
} else {
activeClasses.forEach((cls) => classList.remove(cls));
inactiveClasses.forEach((cls) => classList.add(cls));
}
});
};
const applyFilter = (filterValue) => {
const value = filterValue || "all";
let visibleCount = 0;
cards.forEach((card) => {
const matches = value === "all" || card.dataset.status === value;
card.classList.toggle("hidden", !matches);
if (matches) {
visibleCount += 1;
}
});
if (emptyState) {
emptyState.classList.toggle("hidden", visibleCount > 0);
} }
setButtonState(value);
try {
const url = new URL(window.location.href);
if (value === "all") {
url.searchParams.delete("status");
} else {
url.searchParams.set("status", value);
}
window.history.replaceState({}, "", url);
} catch (error) {
// ignore history errors
}
};
filterButtons.forEach((btn) => {
btn.addEventListener("click", (event) => {
event.preventDefault();
applyFilter(btn.dataset.filterValue || "all");
});
}); });
const initialFilter = new URLSearchParams(window.location.search).get("status") || "all"; document.addEventListener("keydown", (event) => {
applyFilter(initialFilter); if (event.key === "Escape") {
}); document.querySelectorAll("[data-edit-panel]").forEach((panel) => {
if (!panel.classList.contains("hidden")) {
closePanelElement(panel);
}
});
}
});
document.addEventListener("DOMContentLoaded", () => {
const editButtons = Array.from(document.querySelectorAll("[data-open-edit]"));
editButtons.forEach((button) => {
button.addEventListener("click", (event) => {
event.preventDefault();
openPanel(button.dataset.openEdit);
});
});
document.querySelectorAll("[data-close-edit]").forEach((button) => {
button.addEventListener("click", (event) => {
event.preventDefault();
const panel = button.closest("[data-edit-panel]");
if (panel) {
closePanelElement(panel);
}
});
});
const filterButtons = Array.from(document.querySelectorAll("[data-filter-button]"));
const cards = Array.from(document.querySelectorAll("[data-claim-card]"));
const emptyState = document.querySelector("[data-claim-empty]");
if (!filterButtons.length || !cards.length) {
return;
}
const activeClasses = ["bg-brand-600", "text-white", "hover:bg-brand-700"];
const inactiveClasses = ["bg-slate-100", "text-gray-700", "hover:bg-slate-200"];
const setButtonState = (activeValue) => {
filterButtons.forEach((btn) => {
const value = btn.dataset.filterValue || "all";
const isActive = value === activeValue;
btn.setAttribute("aria-pressed", String(isActive));
const classList = btn.classList;
if (isActive) {
inactiveClasses.forEach((cls) => classList.remove(cls));
activeClasses.forEach((cls) => classList.add(cls));
} else {
activeClasses.forEach((cls) => classList.remove(cls));
inactiveClasses.forEach((cls) => classList.add(cls));
}
});
};
const applyFilter = (filterValue) => {
const value = filterValue || "all";
let visibleCount = 0;
cards.forEach((card) => {
const matches = value === "all" || card.dataset.status === value;
card.classList.toggle("hidden", !matches);
if (matches) {
visibleCount += 1;
}
});
if (emptyState) {
emptyState.classList.toggle("hidden", visibleCount > 0);
}
setButtonState(value);
try {
const url = new URL(window.location.href);
if (value === "all") {
url.searchParams.delete("status");
} else {
url.searchParams.set("status", value);
}
window.history.replaceState({}, "", url);
} catch (error) {
// ignore history errors
}
};
filterButtons.forEach((btn) => {
btn.addEventListener("click", (event) => {
event.preventDefault();
applyFilter(btn.dataset.filterValue || "all");
});
});
const initialFilter = new URLSearchParams(window.location.search).get("status") || "all";
applyFilter(initialFilter);
});
})();
</script> </script>
{% endblock %} {% endblock %}

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, ClaimLog, 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,53 @@ 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)
self.assertTrue(claim.logs.filter(action=ClaimLog.Action.PROJECT_CHANGED).exists())
def test_attester_can_edit_details(self):
project = Project.objects.create(name="Event", is_active=True)
claim = self._create_claim(project=project, amount=100)
response = self.client.post(
reverse("claims:admin-list"),
{
"action_type": "edit",
"edit_claim_id": claim.id,
"full_name": "Changed Name",
"email": "changed@example.com",
"account_number": "789-000",
"amount": "555.55",
"currency": Claim.Currency.EUR,
"project": "",
"description": "Updated description",
},
follow=True,
)
self.assertEqual(response.status_code, 200)
claim.refresh_from_db()
self.assertEqual(claim.full_name, "Changed Name")
self.assertEqual(claim.email, "changed@example.com")
self.assertEqual(claim.currency, Claim.Currency.EUR)
self.assertIsNone(claim.project)
edit_log = claim.logs.filter(action=ClaimLog.Action.DETAILS_EDITED).first()
self.assertIsNotNone(edit_log)
self.assertIn("Namn", edit_log.note)

View File

@@ -17,6 +17,7 @@ from django.views.generic import ListView, TemplateView
from .forms import ( from .forms import (
ClaimDecisionForm, ClaimDecisionForm,
ClaimEditForm,
ClaimLineForm, ClaimLineForm,
ClaimantForm, ClaimantForm,
DeleteUserForm, DeleteUserForm,
@@ -24,7 +25,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 +162,8 @@ 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["currency_choices"] = Claim.Currency.choices
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"] = (
@@ -174,6 +177,8 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
action_type = request.POST.get("action_type", "decision") action_type = request.POST.get("action_type", "decision")
if action_type == "payment": if action_type == "payment":
return self._handle_payment(request) return self._handle_payment(request)
if action_type == "edit":
return self._handle_edit(request)
return self._handle_decision(request) return self._handle_decision(request)
def _handle_decision(self, request): def _handle_decision(self, request):
@@ -196,6 +201,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 +214,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,
@@ -212,6 +225,12 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
to_status=claim.status, to_status=claim.status,
note=decision_note, note=decision_note,
) )
if project_changed:
claim.add_log(
action=ClaimLog.Action.PROJECT_CHANGED,
performed_by=request.user,
note=_("Project updated during decision."),
)
return redirect(request.get_full_path()) return redirect(request.get_full_path())
def _handle_payment(self, request): def _handle_payment(self, request):
@@ -241,6 +260,39 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
messages.success(request, _("%(claim)s markerades som betald.") % {"claim": claim}) messages.success(request, _("%(claim)s markerades som betald.") % {"claim": claim})
return redirect(request.get_full_path()) return redirect(request.get_full_path())
def _handle_edit(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())
claim = get_object_or_404(Claim, pk=request.POST.get("edit_claim_id"))
form = ClaimEditForm(request.POST, instance=claim)
if not form.is_valid():
for error in form.errors.get("__all__", []):
messages.error(request, error)
for field, field_errors in form.errors.items():
if field == "__all__":
continue
for error in field_errors:
messages.error(request, f"{form.fields[field].label}: {error}")
return redirect(request.get_full_path())
updated_claim = form.save()
changed_fields = []
for field in form.changed_data:
label = form.fields[field].label or field
changed_fields.append(str(label))
if changed_fields:
note = _("Fields updated: %(fields)s") % {"fields": ", ".join(changed_fields)}
claim.add_log(
action=ClaimLog.Action.DETAILS_EDITED,
performed_by=request.user,
note=note,
)
messages.success(request, _("Informationen uppdaterades."))
else:
messages.info(request, _("Inga förändringar att spara."))
return redirect(request.get_full_path())
def _build_summary(self): def _build_summary(self):
now = timezone.now() now = timezone.now()
last_week = now - timedelta(days=7) last_week = now - timedelta(days=7)

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 21:45+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"
@@ -12,33 +12,43 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: claims/forms.py:19 #: claims/forms.py:19 claims/forms.py:107
msgid "Namn" msgid "Namn"
msgstr "Name" msgstr "Name"
#: claims/forms.py:23 claims/forms.py:86 #: claims/forms.py:23 claims/forms.py:108 claims/forms.py:119
#: claims/templates/claims/dashboard.html:169 #: claims/templates/claims/dashboard.html:176
#: claims/templates/claims/dashboard.html:326
msgid "E-post" msgid "E-post"
msgstr "Email" msgstr "Email"
#: claims/forms.py:28 claims/templates/claims/dashboard.html:157 #: claims/forms.py:28 claims/forms.py:109
#: claims/templates/claims/dashboard.html:164
#: claims/templates/claims/dashboard.html:330
msgid "Kontonummer" msgid "Kontonummer"
msgstr "Account number" msgstr "Account number"
#: claims/forms.py:49 claims/templates/claims/dashboard.html:204 #: claims/forms.py:49 claims/forms.py:113
#: claims/templates/claims/dashboard.html:211
#: claims/templates/claims/dashboard.html:356
msgid "Beskrivning" msgid "Beskrivning"
msgstr "Description" msgstr "Description"
#: claims/forms.py:50 claims/templates/claims/dashboard.html:153 #: claims/forms.py:50 claims/forms.py:110
#: claims/templates/claims/dashboard.html:160
#: claims/templates/claims/dashboard.html:334
#: claims/templates/claims/my_claims.html:23 #: claims/templates/claims/my_claims.html:23
msgid "Belopp" msgid "Belopp"
msgstr "Amount" msgstr "Amount"
#: claims/forms.py:51 #: claims/forms.py:51 claims/forms.py:111
#: claims/templates/claims/dashboard.html:338
msgid "Valuta" msgid "Valuta"
msgstr "Currency" msgstr "Currency"
#: claims/forms.py:52 #: claims/forms.py:52 claims/forms.py:73 claims/forms.py:112
#: claims/templates/claims/dashboard.html:271
#: claims/templates/claims/dashboard.html:346
msgid "Evenemang/Projekt" msgid "Evenemang/Projekt"
msgstr "Project" msgstr "Project"
@@ -54,76 +64,76 @@ msgstr "Approve"
msgid "Neka" msgid "Neka"
msgstr "Reject" msgstr "Reject"
#: claims/forms.py:72 claims/templates/claims/dashboard.html:126 #: claims/forms.py:78 claims/templates/claims/dashboard.html:126
#: claims/templates/claims/dashboard.html:261 #: claims/templates/claims/dashboard.html:268
msgid "Kommentar" msgid "Kommentar"
msgstr "Comment" msgstr "Comment"
#: claims/forms.py:80 #: claims/forms.py:90
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:118
msgid "Användarnamn" msgid "Användarnamn"
msgstr "Username" msgstr "Username"
#: claims/forms.py:87 #: claims/forms.py:120
msgid "Förnamn" msgid "Förnamn"
msgstr "First name" msgstr "First name"
#: claims/forms.py:88 #: claims/forms.py:121
msgid "Efternamn" msgid "Efternamn"
msgstr "Last name" msgstr "Last name"
#: claims/forms.py:89 #: claims/forms.py:122
msgid "Lösenord" msgid "Lösenord"
msgstr "Password" msgstr "Password"
#: claims/forms.py:90 #: claims/forms.py:123
msgid "Bekräfta lösenord" msgid "Bekräfta lösenord"
msgstr "Confirm password" msgstr "Confirm password"
#: claims/forms.py:91 #: claims/forms.py:124
msgid "Administratör (staff)" msgid "Administratör (staff)"
msgstr "Administrator (staff)" msgstr "Administrator (staff)"
#: claims/forms.py:92 #: claims/forms.py:125
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:126
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:131
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:137
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:155 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:156 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:157 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:413
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:417
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:421
msgid "Rejected" msgid "Rejected"
msgstr "Rejected" msgstr "Rejected"
@@ -159,6 +169,14 @@ msgstr "Status changed"
msgid "Marked as paid" msgid "Marked as paid"
msgstr "Marked as paid" msgstr "Marked as paid"
#: claims/models.py:125
msgid "Project changed"
msgstr "Project changed"
#: claims/models.py:126
msgid "Details edited"
msgstr ""
#: claims/templates/claims/base.html:8 #: claims/templates/claims/base.html:8
msgid "Claims" msgid "Claims"
msgstr "Claims" msgstr "Claims"
@@ -241,7 +259,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"
@@ -316,7 +336,7 @@ msgid "Alla"
msgstr "All" msgstr "All"
#: claims/templates/claims/dashboard.html:106 #: claims/templates/claims/dashboard.html:106
#: claims/templates/claims/dashboard.html:165 #: claims/templates/claims/dashboard.html:172
msgid "Skapad" msgid "Skapad"
msgstr "Created" msgstr "Created"
@@ -349,26 +369,32 @@ msgstr "by"
msgid "Ej markerad som betald" msgid "Ej markerad som betald"
msgstr "Not marked as paid" msgstr "Not marked as paid"
#: claims/templates/claims/dashboard.html:145 #: claims/templates/claims/dashboard.html:142
msgid "Redigera"
msgstr "Edit"
#: claims/templates/claims/dashboard.html:152
msgid "Utbetalningsdetaljer" msgid "Utbetalningsdetaljer"
msgstr "Payout details" msgstr "Payout details"
#: claims/templates/claims/dashboard.html:161 #: claims/templates/claims/dashboard.html:168
msgid "Referens (Claim ID)" msgid "Referens (Claim ID)"
msgstr "Reference (Claim ID)" msgstr "Reference (Claim ID)"
#: claims/templates/claims/dashboard.html:173 #: claims/templates/claims/dashboard.html:180
#: claims/templates/claims/my_claims.html:24 #: claims/templates/claims/my_claims.html:24
msgid "Projekt" msgid "Projekt"
msgstr "Project" msgstr "Project"
#: claims/templates/claims/dashboard.html:177 #: claims/templates/claims/dashboard.html:184
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:189
msgid "" msgid ""
"Är du säker på att du har lagt upp betalningen? Markera endast som betald om " "Är du säker på att du har lagt upp betalningen? Markera endast som betald om "
"beloppet skickas till banken." "beloppet skickas till banken."
@@ -376,17 +402,15 @@ msgstr ""
"Are you sure the payment has been scheduled? Only mark as paid if the amount " "Are you sure the payment has been scheduled? Only mark as paid if the amount "
"has been sent to the bank." "has been sent to the bank."
#: claims/templates/claims/dashboard.html:187 #: claims/templates/claims/dashboard.html:194
#, fuzzy
#| msgid "Markerad som betald"
msgid "Markera som betald" msgid "Markera som betald"
msgstr "Marked as paid" msgstr "Mark as paid"
#: claims/templates/claims/dashboard.html:190 #: claims/templates/claims/dashboard.html:197
msgid "Dubbelkolla belopp och kontonummer i panelen innan du bekräftar." msgid "Dubbelkolla belopp och kontonummer i panelen innan du bekräftar."
msgstr "Double-check the amount and account number before confirming." msgstr "Double-check the amount and account number before confirming."
#: claims/templates/claims/dashboard.html:195 #: claims/templates/claims/dashboard.html:202
msgid "" msgid ""
"Intern betalningshantering är av markera betalning i ekonomisystemet och " "Intern betalningshantering är av markera betalning i ekonomisystemet och "
"resetta status vid behov." "resetta status vid behov."
@@ -394,83 +418,115 @@ msgstr ""
"Internal payment handling is off register the payment in the finance " "Internal payment handling is off register the payment in the finance "
"system and reset the status if needed." "system and reset the status if needed."
#: claims/templates/claims/dashboard.html:212 #: claims/templates/claims/dashboard.html:219
msgid "Visa kvitto" msgid "Visa kvitto"
msgstr "View receipt" msgstr "View receipt"
#: claims/templates/claims/dashboard.html:215 #: claims/templates/claims/dashboard.html:222
msgid "Inget kvitto bifogat" msgid "Inget kvitto bifogat"
msgstr "No receipt attached" msgstr "No receipt attached"
#: claims/templates/claims/dashboard.html:217 #: claims/templates/claims/dashboard.html:224
msgid "Senast uppdaterad" msgid "Senast uppdaterad"
msgstr "Last updated" msgstr "Last updated"
#: claims/templates/claims/dashboard.html:222 #: claims/templates/claims/dashboard.html:229
msgid "Logg" msgid "Logg"
msgstr "Log" msgstr "Log"
#: claims/templates/claims/dashboard.html:229 #: claims/templates/claims/dashboard.html:236
#: claims/templates/claims/my_claims.html:62 #: claims/templates/claims/my_claims.html:62
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
#: claims/templates/claims/dashboard.html:235 #: claims/templates/claims/dashboard.html:242
msgid "Av" msgid "Av"
msgstr "By" msgstr "By"
#: claims/templates/claims/dashboard.html:239 #: claims/templates/claims/dashboard.html:246
#: claims/templates/claims/my_claims.html:69 #: claims/templates/claims/my_claims.html:69
msgid "Ingen logg än." msgid "Ingen logg än."
msgstr "No log entries yet." msgstr "No log entries yet."
#: claims/templates/claims/dashboard.html:247 #: claims/templates/claims/dashboard.html:254
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."
msgstr "The claim is marked as paid. Decision/comments are locked." msgstr "The claim is marked as paid. Decision/comments are locked."
#: claims/templates/claims/dashboard.html:254 #: claims/templates/claims/dashboard.html:261
msgid "Åtgärd" msgid "Åtgärd"
msgstr "Action" msgstr "Action"
#: claims/templates/claims/dashboard.html:266 #: claims/templates/claims/dashboard.html:273
msgid "Behåll nuvarande"
msgstr "Keep current"
#: claims/templates/claims/dashboard.html:278
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:282
msgid "Uppdatera beslut" msgid "Uppdatera beslut"
msgstr "Update decision" msgstr "Update decision"
#: claims/templates/claims/dashboard.html:277 #: claims/templates/claims/dashboard.html:293
#: 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:294
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:300
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:301
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:309
msgid "Redigera utlägg"
msgstr "Edit claim"
#: claims/templates/claims/dashboard.html:313
msgid "Stäng"
msgstr "Close"
#: claims/templates/claims/dashboard.html:348
msgid "Ingen"
msgstr "None"
#: claims/templates/claims/dashboard.html:361
msgid "Avbryt"
msgstr "Cancel"
#: claims/templates/claims/dashboard.html:364
msgid "Spara ändringar"
msgstr "Save changes"
#: claims/templates/claims/dashboard.html:370
msgid "Aktivera JavaScript för att kunna redigera uppgifter direkt här."
msgstr "Enable JavaScript to edit the information directly here."
#: claims/templates/claims/dashboard.html:381
msgid "Senaste inskick" msgid "Senaste inskick"
msgstr "Latest submissions" msgstr "Latest submissions"
#: claims/templates/claims/dashboard.html:294 #: claims/templates/claims/dashboard.html:382
msgid "Aktivitet" msgid "Aktivitet"
msgstr "Activity" msgstr "Activity"
#: claims/templates/claims/dashboard.html:312 #: claims/templates/claims/dashboard.html:400
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:408
msgid "Statusfördelning" msgid "Statusfördelning"
msgstr "Status breakdown" msgstr "Status breakdown"
#: claims/templates/claims/dashboard.html:321 #: claims/templates/claims/dashboard.html:409
msgid "Snabbstatistik" msgid "Snabbstatistik"
msgstr "Quick stats" msgstr "Quick stats"
@@ -814,100 +870,115 @@ msgstr ""
msgid "Filens innehåll matchar inte förväntat format." msgid "Filens innehåll matchar inte förväntat format."
msgstr "" msgstr ""
#: claims/views.py:126 #: claims/views.py:127
#, python-brace-format #, python-brace-format
msgid "{} utlägg skickade in." msgid "{} utlägg skickade in."
msgstr "" msgstr ""
#: claims/views.py:129 #: claims/views.py:130
msgid "Inga utlägg kunde sparas. Fyll i minst en rad." msgid "Inga utlägg kunde sparas. Fyll i minst en rad."
msgstr "" msgstr ""
#: claims/views.py:131 #: claims/views.py:132
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:186 claims/views.py:241 claims/views.py:265
#, 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:200
#, 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:212
#, python-format #, python-format
msgid "%(claim)s markerades som godkänd." msgid "%(claim)s markerades som godkänd."
msgstr "" msgstr "%(claim)s was marked as approved."
#: claims/views.py:205 #: claims/views.py:215
#, fuzzy, python-format #, python-format
#| msgid "Ej markerad som betald"
msgid "%(claim)s markerades som nekad." msgid "%(claim)s markerades som nekad."
msgstr "Not marked as paid" msgstr "%(claim)s was marked as rejected."
#: claims/views.py:219 #: claims/views.py:232
msgid "Project updated during decision."
msgstr "Project updated during decision."
#: claims/views.py:238
msgid "Betalningshantering är inte aktiverad." msgid "Betalningshantering är inte aktiverad."
msgstr "" msgstr "Payment handling is not enabled."
#: claims/views.py:227 #: claims/views.py:246
msgid "Endast godkända utlägg kan markeras som betalda." msgid "Endast godkända utlägg kan markeras som betalda."
msgstr "" msgstr "Only approved claims can be marked as paid."
#: claims/views.py:230 #: claims/views.py:249
msgid "Detta utlägg är redan markerat som betalt." msgid "Detta utlägg är redan markerat som betalt."
msgstr "" msgstr "This claim is already marked as paid."
#: claims/views.py:241 #: claims/views.py:260
#, fuzzy, python-format #, python-format
#| msgid "Ej markerad som betald"
msgid "%(claim)s markerades som betald." msgid "%(claim)s markerades som betald."
msgstr "Not marked as paid" msgstr "%(claim)s was marked as paid."
#: claims/views.py:307 #: claims/views.py:287
#, python-format
msgid "Fields updated: %(fields)s"
msgstr "Fields updated: %(fields)s"
#: claims/views.py:293
msgid "Informationen uppdaterades."
msgstr "Information updated."
#: claims/views.py:295
msgid "Inga förändringar att spara."
msgstr "No changes to save."
#: claims/views.py:361
msgid "Du saknar behörighet för åtgärden." msgid "Du saknar behörighet för åtgärden."
msgstr "" msgstr "You do not have permission to perform this action."
#: claims/views.py:354 #: claims/views.py:408
#, 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:419
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:425
#, 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:427
#, 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:437
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:439
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:442
#, 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:445
msgid "Okänd åtgärd." msgid "Okänd åtgärd."
msgstr "" msgstr ""