Compare commits
10 Commits
a953092718
...
559ed671f3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
559ed671f3 | ||
|
|
9499eb6395 | ||
|
|
968150b074 | ||
|
|
c189fd053c | ||
|
|
3323ffd82e | ||
|
|
f42381a9a0 | ||
|
|
78377a7ae9 | ||
|
|
caf3df24cf | ||
|
|
0d68c75fef | ||
|
|
70aeca6187 |
@@ -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. 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.
|
||||
- **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,11 +67,21 @@ 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"),
|
||||
)
|
||||
|
||||
decision_note = forms.CharField(
|
||||
required=False,
|
||||
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):
|
||||
cleaned = super().clean()
|
||||
action = cleaned.get("action")
|
||||
@@ -81,6 +91,29 @@ class ClaimDecisionForm(forms.Form):
|
||||
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):
|
||||
username = forms.CharField(max_length=150, label=_("Användarnamn"))
|
||||
email = forms.EmailField(required=False, label=_("E-post"))
|
||||
|
||||
@@ -122,6 +122,8 @@ class ClaimLog(models.Model):
|
||||
CREATED = "created", _("Submitted")
|
||||
STATUS_CHANGED = "status_changed", _("Status changed")
|
||||
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)
|
||||
action = models.CharField(max_length=32, choices=Action.choices)
|
||||
@@ -147,4 +149,3 @@ class ClaimLog(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_action_display()} ({self.created_at:%Y-%m-%d %H:%M})"
|
||||
|
||||
|
||||
@@ -85,6 +85,82 @@
|
||||
{% endfor %}
|
||||
</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>
|
||||
|
||||
<div class="space-y-6" data-claim-list>
|
||||
@@ -118,25 +194,32 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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 %}">
|
||||
{{ claim.get_status_display }}
|
||||
</span>
|
||||
{% if claim.decision_note %}
|
||||
<p class="text-xs text-gray-500">{% trans "Kommentar" %}: {{ claim.decision_note }}</p>
|
||||
<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 %}">
|
||||
{{ claim.get_status_display }}
|
||||
</span>
|
||||
{% if claim.decision_note %}
|
||||
<p class="text-xs text-gray-500">{% trans "Kommentar" %}: {{ claim.decision_note }}</p>
|
||||
{% endif %}
|
||||
{% if payments_enabled and claim.status == 'approved' %}
|
||||
{% if claim.is_paid %}
|
||||
<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" }}
|
||||
{% if claim.paid_by %}{% trans "av" %} {{ claim.paid_by.get_username }}{% endif %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-xs text-gray-500">{% trans "Ej markerad som betald" %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if claim.is_paid %}
|
||||
<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" }}
|
||||
{% if claim.paid_by %}{% trans "av" %} {{ claim.paid_by.get_username }}{% endif %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-xs text-gray-500">{% trans "Ej markerad som betald" %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if can_change %}
|
||||
<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' %}
|
||||
<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 %}
|
||||
</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>
|
||||
@@ -339,73 +431,125 @@
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
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;
|
||||
(function () {
|
||||
function openPanel(id) {
|
||||
const panel = document.querySelector(`[data-edit-panel="${id}"]`);
|
||||
if (!panel) return;
|
||||
panel.classList.remove("hidden");
|
||||
panel.classList.add("flex");
|
||||
panel.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
|
||||
const activeClasses = ["bg-brand-600", "text-white", "hover:bg-brand-700"];
|
||||
const inactiveClasses = ["bg-slate-100", "text-gray-700", "hover:bg-slate-200"];
|
||||
function closePanelElement(panel) {
|
||||
panel.classList.add("hidden");
|
||||
panel.classList.remove("flex");
|
||||
panel.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
|
||||
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);
|
||||
document.addEventListener("click", (event) => {
|
||||
const backdrop = event.target.closest("[data-edit-backdrop]");
|
||||
if (backdrop && event.target === backdrop) {
|
||||
closePanelElement(backdrop);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
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>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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, ClaimLog, 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,53 @@ 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)
|
||||
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)
|
||||
|
||||
@@ -17,6 +17,7 @@ from django.views.generic import ListView, TemplateView
|
||||
|
||||
from .forms import (
|
||||
ClaimDecisionForm,
|
||||
ClaimEditForm,
|
||||
ClaimLineForm,
|
||||
ClaimantForm,
|
||||
DeleteUserForm,
|
||||
@@ -24,7 +25,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 +162,8 @@ 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["currency_choices"] = Claim.Currency.choices
|
||||
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"] = (
|
||||
@@ -174,6 +177,8 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
action_type = request.POST.get("action_type", "decision")
|
||||
if action_type == "payment":
|
||||
return self._handle_payment(request)
|
||||
if action_type == "edit":
|
||||
return self._handle_edit(request)
|
||||
return self._handle_decision(request)
|
||||
|
||||
def _handle_decision(self, request):
|
||||
@@ -196,6 +201,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 +214,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,
|
||||
@@ -212,6 +225,12 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
to_status=claim.status,
|
||||
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())
|
||||
|
||||
def _handle_payment(self, request):
|
||||
@@ -241,6 +260,39 @@ class ClaimDashboardView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
messages.success(request, _("%(claim)s markerades som betald.") % {"claim": claim})
|
||||
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):
|
||||
now = timezone.now()
|
||||
last_week = now - timedelta(days=7)
|
||||
|
||||
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 21:45+0100\n"
|
||||
"PO-Revision-Date: 2025-11-08 23:40+0100\n"
|
||||
"Last-Translator: ChatGPT <noreply@example.com>\n"
|
||||
"Language-Team: English\n"
|
||||
@@ -12,33 +12,43 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: claims/forms.py:19
|
||||
#: claims/forms.py:19 claims/forms.py:107
|
||||
msgid "Namn"
|
||||
msgstr "Name"
|
||||
|
||||
#: claims/forms.py:23 claims/forms.py:86
|
||||
#: claims/templates/claims/dashboard.html:169
|
||||
#: claims/forms.py:23 claims/forms.py:108 claims/forms.py:119
|
||||
#: claims/templates/claims/dashboard.html:176
|
||||
#: claims/templates/claims/dashboard.html:326
|
||||
msgid "E-post"
|
||||
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"
|
||||
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"
|
||||
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
|
||||
msgid "Belopp"
|
||||
msgstr "Amount"
|
||||
|
||||
#: claims/forms.py:51
|
||||
#: claims/forms.py:51 claims/forms.py:111
|
||||
#: claims/templates/claims/dashboard.html:338
|
||||
msgid "Valuta"
|
||||
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"
|
||||
msgstr "Project"
|
||||
|
||||
@@ -54,76 +64,76 @@ msgstr "Approve"
|
||||
msgid "Neka"
|
||||
msgstr "Reject"
|
||||
|
||||
#: claims/forms.py:72 claims/templates/claims/dashboard.html:126
|
||||
#: claims/templates/claims/dashboard.html:261
|
||||
#: claims/forms.py:78 claims/templates/claims/dashboard.html:126
|
||||
#: claims/templates/claims/dashboard.html:268
|
||||
msgid "Kommentar"
|
||||
msgstr "Comment"
|
||||
|
||||
#: claims/forms.py:80
|
||||
#: claims/forms.py:90
|
||||
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:118
|
||||
msgid "Användarnamn"
|
||||
msgstr "Username"
|
||||
|
||||
#: claims/forms.py:87
|
||||
#: claims/forms.py:120
|
||||
msgid "Förnamn"
|
||||
msgstr "First name"
|
||||
|
||||
#: claims/forms.py:88
|
||||
#: claims/forms.py:121
|
||||
msgid "Efternamn"
|
||||
msgstr "Last name"
|
||||
|
||||
#: claims/forms.py:89
|
||||
#: claims/forms.py:122
|
||||
msgid "Lösenord"
|
||||
msgstr "Password"
|
||||
|
||||
#: claims/forms.py:90
|
||||
#: claims/forms.py:123
|
||||
msgid "Bekräfta lösenord"
|
||||
msgstr "Confirm password"
|
||||
|
||||
#: claims/forms.py:91
|
||||
#: claims/forms.py:124
|
||||
msgid "Administratör (staff)"
|
||||
msgstr "Administrator (staff)"
|
||||
|
||||
#: claims/forms.py:92
|
||||
#: claims/forms.py:125
|
||||
msgid "Ge behörighet att se utlägg"
|
||||
msgstr "Allow viewing claims"
|
||||
|
||||
#: claims/forms.py:93
|
||||
#: claims/forms.py:126
|
||||
msgid "Ge behörighet att besluta utlägg"
|
||||
msgstr "Allow deciding claims"
|
||||
|
||||
#: claims/forms.py:98
|
||||
#: claims/forms.py:131
|
||||
msgid "Användarnamnet är upptaget."
|
||||
msgstr "That username is already taken."
|
||||
|
||||
#: claims/forms.py:104
|
||||
#: claims/forms.py:137
|
||||
msgid "Lösenorden matchar inte."
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
msgstr "Pending"
|
||||
|
||||
#: claims/models.py:30 claims/templates/claims/dashboard.html:329
|
||||
#: claims/models.py:30 claims/templates/claims/dashboard.html:417
|
||||
msgid "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"
|
||||
msgstr "Rejected"
|
||||
|
||||
@@ -159,6 +169,14 @@ msgstr "Status changed"
|
||||
msgid "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
|
||||
msgid "Claims"
|
||||
msgstr "Claims"
|
||||
@@ -241,7 +259,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"
|
||||
@@ -316,7 +336,7 @@ msgid "Alla"
|
||||
msgstr "All"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:106
|
||||
#: claims/templates/claims/dashboard.html:165
|
||||
#: claims/templates/claims/dashboard.html:172
|
||||
msgid "Skapad"
|
||||
msgstr "Created"
|
||||
|
||||
@@ -349,26 +369,32 @@ msgstr "by"
|
||||
msgid "Ej markerad som betald"
|
||||
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"
|
||||
msgstr "Payout details"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:161
|
||||
#: claims/templates/claims/dashboard.html:168
|
||||
msgid "Referens (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
|
||||
msgid "Projekt"
|
||||
msgstr "Project"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:177
|
||||
#: claims/templates/claims/dashboard.html:184
|
||||
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
|
||||
#: claims/templates/claims/dashboard.html:189
|
||||
msgid ""
|
||||
"Är du säker på att du har lagt upp betalningen? Markera endast som betald om "
|
||||
"beloppet skickas till banken."
|
||||
@@ -376,17 +402,15 @@ msgstr ""
|
||||
"Are you sure the payment has been scheduled? Only mark as paid if the amount "
|
||||
"has been sent to the bank."
|
||||
|
||||
#: claims/templates/claims/dashboard.html:187
|
||||
#, fuzzy
|
||||
#| msgid "Markerad som betald"
|
||||
#: claims/templates/claims/dashboard.html:194
|
||||
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."
|
||||
msgstr "Double-check the amount and account number before confirming."
|
||||
|
||||
#: claims/templates/claims/dashboard.html:195
|
||||
#: claims/templates/claims/dashboard.html:202
|
||||
msgid ""
|
||||
"Intern betalningshantering är av – markera betalning i ekonomisystemet och "
|
||||
"resetta status vid behov."
|
||||
@@ -394,83 +418,115 @@ msgstr ""
|
||||
"Internal payment handling is off – register the payment in the finance "
|
||||
"system and reset the status if needed."
|
||||
|
||||
#: claims/templates/claims/dashboard.html:212
|
||||
#: claims/templates/claims/dashboard.html:219
|
||||
msgid "Visa kvitto"
|
||||
msgstr "View receipt"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:215
|
||||
#: claims/templates/claims/dashboard.html:222
|
||||
msgid "Inget kvitto bifogat"
|
||||
msgstr "No receipt attached"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:217
|
||||
#: claims/templates/claims/dashboard.html:224
|
||||
msgid "Senast uppdaterad"
|
||||
msgstr "Last updated"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:222
|
||||
#: claims/templates/claims/dashboard.html:229
|
||||
msgid "Logg"
|
||||
msgstr "Log"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:229
|
||||
#: claims/templates/claims/dashboard.html:236
|
||||
#: claims/templates/claims/my_claims.html:62
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:235
|
||||
#: claims/templates/claims/dashboard.html:242
|
||||
msgid "Av"
|
||||
msgstr "By"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:239
|
||||
#: claims/templates/claims/dashboard.html:246
|
||||
#: claims/templates/claims/my_claims.html:69
|
||||
msgid "Ingen logg än."
|
||||
msgstr "No log entries yet."
|
||||
|
||||
#: claims/templates/claims/dashboard.html:247
|
||||
#: claims/templates/claims/dashboard.html:254
|
||||
msgid ""
|
||||
"Utlägget är markerat som betalt. Ändringar av beslut/kommentar är låsta."
|
||||
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"
|
||||
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"
|
||||
msgstr "Update decision"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:277
|
||||
#: claims/templates/claims/dashboard.html:293
|
||||
#: 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:294
|
||||
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:300
|
||||
msgid "Inga utlägg matchar filtret"
|
||||
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."
|
||||
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"
|
||||
msgstr "Latest submissions"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:294
|
||||
#: claims/templates/claims/dashboard.html:382
|
||||
msgid "Aktivitet"
|
||||
msgstr "Activity"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:312
|
||||
#: claims/templates/claims/dashboard.html:400
|
||||
msgid "Inga aktiviteter än."
|
||||
msgstr "No activity yet."
|
||||
|
||||
#: claims/templates/claims/dashboard.html:320
|
||||
#: claims/templates/claims/dashboard.html:408
|
||||
msgid "Statusfördelning"
|
||||
msgstr "Status breakdown"
|
||||
|
||||
#: claims/templates/claims/dashboard.html:321
|
||||
#: claims/templates/claims/dashboard.html:409
|
||||
msgid "Snabbstatistik"
|
||||
msgstr "Quick stats"
|
||||
|
||||
@@ -814,100 +870,115 @@ msgstr ""
|
||||
msgid "Filens innehåll matchar inte förväntat format."
|
||||
msgstr ""
|
||||
|
||||
#: claims/views.py:126
|
||||
#: claims/views.py:127
|
||||
#, python-brace-format
|
||||
msgid "{} utlägg skickade in."
|
||||
msgstr ""
|
||||
|
||||
#: claims/views.py:129
|
||||
#: claims/views.py:130
|
||||
msgid "Inga utlägg kunde sparas. Fyll i minst en rad."
|
||||
msgstr ""
|
||||
|
||||
#: claims/views.py:131
|
||||
#: claims/views.py:132
|
||||
msgid "Kunde inte spara utläggen. Kontrollera formuläret."
|
||||
msgstr ""
|
||||
|
||||
#: claims/views.py:181 claims/views.py:222
|
||||
#: claims/views.py:186 claims/views.py:241 claims/views.py:265
|
||||
#, 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:200
|
||||
#, 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:212
|
||||
#, python-format
|
||||
msgid "%(claim)s markerades som godkänd."
|
||||
msgstr ""
|
||||
msgstr "%(claim)s was marked as approved."
|
||||
|
||||
#: claims/views.py:205
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Ej markerad som betald"
|
||||
#: claims/views.py:215
|
||||
#, python-format
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
msgstr ""
|
||||
msgstr "This claim is already marked as paid."
|
||||
|
||||
#: claims/views.py:241
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Ej markerad som betald"
|
||||
#: claims/views.py:260
|
||||
#, python-format
|
||||
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."
|
||||
msgstr ""
|
||||
msgstr "You do not have permission to perform this action."
|
||||
|
||||
#: claims/views.py:354
|
||||
#: claims/views.py:408
|
||||
#, python-format
|
||||
msgid "Användaren %(user)s skapades."
|
||||
msgstr ""
|
||||
|
||||
#: claims/views.py:365
|
||||
#: claims/views.py:419
|
||||
msgid "Du kan inte ta bort din egen staff-status."
|
||||
msgstr ""
|
||||
|
||||
#: claims/views.py:371
|
||||
#: claims/views.py:425
|
||||
#, python-format
|
||||
msgid "Behörigheter uppdaterades för %(user)s."
|
||||
msgstr ""
|
||||
|
||||
#: claims/views.py:373
|
||||
#: claims/views.py:427
|
||||
#, fuzzy
|
||||
#| msgid "Justera behörigheter"
|
||||
msgid "Kunde inte uppdatera behörigheter."
|
||||
msgstr "Adjust permissions"
|
||||
|
||||
#: claims/views.py:383
|
||||
#: claims/views.py:437
|
||||
msgid "Du kan inte ta bort ditt eget konto."
|
||||
msgstr ""
|
||||
|
||||
#: claims/views.py:385
|
||||
#: claims/views.py:439
|
||||
msgid "Du kan inte ta bort en superuser via detta gränssnitt."
|
||||
msgstr ""
|
||||
|
||||
#: claims/views.py:388
|
||||
#: claims/views.py:442
|
||||
#, fuzzy
|
||||
#| msgid "Användare"
|
||||
msgid "Användaren togs bort."
|
||||
msgstr "Users"
|
||||
|
||||
#: claims/views.py:391
|
||||
#: claims/views.py:445
|
||||
msgid "Okänd åtgärd."
|
||||
msgstr ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user