Squash merge feature/email-notifications into beta
This commit is contained in:
53
claims/email_utils.py
Normal file
53
claims/email_utils.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def emails_enabled():
|
||||
return getattr(settings, "CLAIMS_EMAIL_ENABLED", False)
|
||||
|
||||
|
||||
def _send(subject: str, template: str, context: dict, recipient: str):
|
||||
if not emails_enabled():
|
||||
logger.info("Email disabled - skipping send to %s", recipient)
|
||||
return
|
||||
html_message = render_to_string(template, context)
|
||||
plain_message = strip_tags(html_message)
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message=plain_message,
|
||||
from_email=settings.CLAIMS_EMAIL_FROM,
|
||||
recipient_list=[recipient],
|
||||
html_message=html_message,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
|
||||
def send_claimant_confirmation_email(claim):
|
||||
if not claim.email:
|
||||
return
|
||||
context = {"claim": claim}
|
||||
_send(
|
||||
subject="Din utläggsbegäran är mottagen",
|
||||
template="emails/claim_submitted_claimant.html",
|
||||
context=context,
|
||||
recipient=claim.email,
|
||||
)
|
||||
|
||||
|
||||
def notify_admin_of_claim(claim):
|
||||
recipient = settings.CLAIMS_ADMIN_NOTIFICATION_EMAIL
|
||||
if not recipient:
|
||||
return
|
||||
context = {"claim": claim}
|
||||
_send(
|
||||
subject="Nytt utlägg inskickat",
|
||||
template="emails/claim_submitted_admin.html",
|
||||
context=context,
|
||||
recipient=recipient,
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model, password_validation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Claim, Project
|
||||
|
||||
@@ -15,16 +16,16 @@ FILE_CLASSES = "mt-1 block w-full text-sm text-gray-700 file:mr-4 file:rounded-m
|
||||
class ClaimantForm(forms.Form):
|
||||
full_name = forms.CharField(
|
||||
max_length=255,
|
||||
label="Namn",
|
||||
label=_("Namn"),
|
||||
widget=forms.TextInput(attrs={"class": INPUT_CLASSES}),
|
||||
)
|
||||
email = forms.EmailField(
|
||||
label="E-post",
|
||||
label=_("E-post"),
|
||||
widget=forms.EmailInput(attrs={"class": INPUT_CLASSES}),
|
||||
)
|
||||
account_number = forms.CharField(
|
||||
max_length=50,
|
||||
label="Kontonummer",
|
||||
label=_("Kontonummer"),
|
||||
widget=forms.TextInput(attrs={"class": INPUT_CLASSES}),
|
||||
)
|
||||
|
||||
@@ -45,11 +46,11 @@ class ClaimLineForm(forms.ModelForm):
|
||||
model = Claim
|
||||
fields = ["description", "amount", "currency", "project", "receipt"]
|
||||
labels = {
|
||||
"description": "Beskrivning",
|
||||
"amount": "Belopp",
|
||||
"currency": "Valuta",
|
||||
"project": "Evenemang/Projekt",
|
||||
"receipt": "Kvitto",
|
||||
"description": _("Beskrivning"),
|
||||
"amount": _("Belopp"),
|
||||
"currency": _("Valuta"),
|
||||
"project": _("Evenemang/Projekt"),
|
||||
"receipt": _("Kvitto"),
|
||||
}
|
||||
widgets = {
|
||||
"description": forms.Textarea(attrs={"rows": 3, "class": TEXTAREA_CLASSES}),
|
||||
@@ -60,15 +61,15 @@ class ClaimDecisionForm(forms.Form):
|
||||
ACTION_APPROVE = "approve"
|
||||
ACTION_REJECT = "reject"
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_APPROVE, "Godkänn"),
|
||||
(ACTION_REJECT, "Neka"),
|
||||
(ACTION_APPROVE, _("Godkänn")),
|
||||
(ACTION_REJECT, _("Neka")),
|
||||
)
|
||||
|
||||
claim_id = forms.IntegerField(widget=forms.HiddenInput)
|
||||
action = forms.ChoiceField(choices=ACTION_CHOICES)
|
||||
decision_note = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={"rows": 2, "placeholder": "Kommentar"}),
|
||||
widget=forms.Textarea(attrs={"rows": 2, "placeholder": _("Kommentar")}),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
@@ -76,31 +77,31 @@ class ClaimDecisionForm(forms.Form):
|
||||
action = cleaned.get("action")
|
||||
note = cleaned.get("decision_note", "").strip()
|
||||
if action == self.ACTION_REJECT and not note:
|
||||
self.add_error("decision_note", "Kommentar krävs när du nekar ett utlägg.")
|
||||
self.add_error("decision_note", _("Kommentar krävs när du nekar ett utlägg."))
|
||||
return cleaned
|
||||
|
||||
|
||||
class UserManagementForm(forms.Form):
|
||||
username = forms.CharField(max_length=150, label="Användarnamn")
|
||||
email = forms.EmailField(required=False, label="E-post")
|
||||
first_name = forms.CharField(max_length=150, required=False, label="Förnamn")
|
||||
last_name = forms.CharField(max_length=150, required=False, label="Efternamn")
|
||||
password1 = forms.CharField(widget=forms.PasswordInput, label="Lösenord")
|
||||
password2 = forms.CharField(widget=forms.PasswordInput, label="Bekräfta lösenord")
|
||||
is_staff = forms.BooleanField(required=False, initial=True, label="Administratör (staff)")
|
||||
grant_view = forms.BooleanField(required=False, initial=True, label="Ge behörighet att se utlägg")
|
||||
grant_change = forms.BooleanField(required=False, initial=True, label="Ge behörighet att besluta utlägg")
|
||||
username = forms.CharField(max_length=150, label=_("Användarnamn"))
|
||||
email = forms.EmailField(required=False, label=_("E-post"))
|
||||
first_name = forms.CharField(max_length=150, required=False, label=_("Förnamn"))
|
||||
last_name = forms.CharField(max_length=150, required=False, label=_("Efternamn"))
|
||||
password1 = forms.CharField(widget=forms.PasswordInput, label=_("Lösenord"))
|
||||
password2 = forms.CharField(widget=forms.PasswordInput, label=_("Bekräfta lösenord"))
|
||||
is_staff = forms.BooleanField(required=False, initial=True, label=_("Administratör (staff)"))
|
||||
grant_view = forms.BooleanField(required=False, initial=True, label=_("Ge behörighet att se utlägg"))
|
||||
grant_change = forms.BooleanField(required=False, initial=True, label=_("Ge behörighet att besluta utlägg"))
|
||||
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data["username"]
|
||||
if User.objects.filter(username=username).exists():
|
||||
raise forms.ValidationError("Användarnamnet är upptaget.")
|
||||
raise forms.ValidationError(_("Användarnamnet är upptaget."))
|
||||
return username
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
if cleaned.get("password1") != cleaned.get("password2"):
|
||||
self.add_error("password2", "Lösenorden matchar inte.")
|
||||
self.add_error("password2", _("Lösenorden matchar inte."))
|
||||
password = cleaned.get("password1")
|
||||
if password:
|
||||
temp_user = User(
|
||||
@@ -118,9 +119,9 @@ class UserManagementForm(forms.Form):
|
||||
|
||||
class UserPermissionForm(forms.Form):
|
||||
user_id = forms.IntegerField(widget=forms.HiddenInput)
|
||||
is_staff = forms.BooleanField(required=False, label="Admin/staff")
|
||||
grant_view = forms.BooleanField(required=False, label="Får se utlägg")
|
||||
grant_change = forms.BooleanField(required=False, label="Får besluta utlägg")
|
||||
is_staff = forms.BooleanField(required=False, label=_("Admin/staff"))
|
||||
grant_view = forms.BooleanField(required=False, label=_("Får se utlägg"))
|
||||
grant_change = forms.BooleanField(required=False, label=_("Får besluta utlägg"))
|
||||
|
||||
|
||||
class DeleteUserForm(forms.Form):
|
||||
|
||||
1
claims/management/__init__.py
Normal file
1
claims/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Package marker
|
||||
1
claims/management/commands/__init__.py
Normal file
1
claims/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Package marker
|
||||
35
claims/management/commands/reset_claims.py
Normal file
35
claims/management/commands/reset_claims.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
|
||||
from claims.models import Claim
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Tar bort samtliga utlägg (claims) men lämnar användarkonton orörda."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
action="store_true",
|
||||
help="Utför rensningen utan interaktiv bekräftelse.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if not options["noinput"]:
|
||||
confirm = input(
|
||||
"Detta tar bort alla claims inklusive loggar och kvitton men lämnar konton. Fortsätt? (skriv 'ja'): "
|
||||
)
|
||||
if confirm.lower() != "ja":
|
||||
self.stdout.write(self.style.WARNING("Avbrutet."))
|
||||
return
|
||||
|
||||
count = Claim.objects.count()
|
||||
with transaction.atomic():
|
||||
for claim in Claim.objects.iterator():
|
||||
if claim.receipt:
|
||||
claim.receipt.delete(save=False)
|
||||
Claim.objects.all().delete()
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Raderade {count} claims och tillhörande loggar/kvitton.")
|
||||
)
|
||||
@@ -1,4 +1,7 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -90,6 +93,22 @@ class Claim(models.Model):
|
||||
performed_by=performed_by,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.receipt and not kwargs.pop("skip_receipt_rename", False):
|
||||
original_name = self.receipt.name or ""
|
||||
ext = original_name.rsplit(".", 1)[-1] if "." in original_name else "dat"
|
||||
new_name = self._generate_unique_receipt_name(ext)
|
||||
self.receipt.name = new_name
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _generate_unique_receipt_name(self, ext):
|
||||
ext = ext.lower()
|
||||
for _ in range(10):
|
||||
candidate = f"receipts/{uuid.uuid4().hex}.{ext}"
|
||||
if not default_storage.exists(candidate):
|
||||
return candidate
|
||||
return f"receipts/{uuid.uuid4().hex}.{ext}"
|
||||
|
||||
|
||||
class ClaimLog(models.Model):
|
||||
class Action(models.TextChoices):
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
{% extends "claims/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Admin – Utlägg{% endblock %}
|
||||
{% block title %}{% trans "Admin – Utlägg" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="space-y-8 py-6">
|
||||
<header class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">Översikt</p>
|
||||
<h1 class="text-3xl font-semibold text-gray-900">Inkomna utlägg</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Filtrera på status, granska kvitton och uppdatera beslut direkt i listan.</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Översikt" %}</p>
|
||||
<h1 class="text-3xl font-semibold text-gray-900">{% trans "Inkomna utlägg" %}</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">{% trans "Filtrera på status, granska kvitton och uppdatera beslut direkt i listan." %}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% with selected=status_filter %}
|
||||
{% with filters="all"|add:"," %}
|
||||
{% endwith %}
|
||||
{% with statuses=status_choices %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
<a href="?status=all"
|
||||
class="rounded-full px-4 py-2 text-sm font-semibold {% if status_filter == 'all' %}bg-brand-600 text-white{% else %}bg-white text-gray-700 shadow-sm ring-1 ring-gray-200 hover:bg-gray-50{% endif %}">
|
||||
Alla
|
||||
{% trans "Alla" %}
|
||||
</a>
|
||||
{% for value, label in status_choices %}
|
||||
<a href="?status={{ value }}"
|
||||
@@ -45,17 +40,17 @@
|
||||
{{ claim.project }}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="text-xs text-gray-400">Skapad {{ claim.created_at|date:"Y-m-d H:i" }}</span>
|
||||
<span class="text-xs text-gray-400">{% trans "Skapad" %} {{ claim.created_at|date:"Y-m-d H:i" }}</span>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">Person</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Person" %}</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">{{ claim.full_name }}</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ claim.email }} · Konto: <span class="font-mono text-gray-900">{{ claim.account_number }}</span><br>
|
||||
{{ claim.email }} · {% trans "Konto" %}: <span class="font-mono text-gray-900">{{ claim.account_number }}</span><br>
|
||||
{% if claim.submitted_by %}
|
||||
<span class="text-xs uppercase tracking-wide text-green-600">Inloggad användare: {{ claim.submitted_by.get_username }}</span>
|
||||
<span class="text-xs uppercase tracking-wide text-green-600">{% trans "Inloggad användare" %}: {{ claim.submitted_by.get_username }}</span>
|
||||
{% else %}
|
||||
<span class="text-xs uppercase tracking-wide text-gray-500">Inskickad av gäst</span>
|
||||
<span class="text-xs uppercase tracking-wide text-gray-500">{% trans "Inskickad av gäst" %}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
@@ -65,16 +60,16 @@
|
||||
{{ claim.get_status_display }}
|
||||
</span>
|
||||
{% if claim.decision_note %}
|
||||
<p class="text-xs text-gray-500">Kommentar: {{ claim.decision_note }}</p>
|
||||
<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">
|
||||
Betald {{ claim.paid_at|date:"Y-m-d H:i" }}
|
||||
{% if claim.paid_by %}av {{ claim.paid_by.get_username }}{% endif %}
|
||||
{% 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">Ej markerad som betald</span>
|
||||
<span class="text-xs text-gray-500">{% trans "Ej markerad som betald" %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -84,25 +79,25 @@
|
||||
<div class="mx-6 mt-4 rounded-2xl border border-green-100 bg-green-50 px-6 py-4 text-sm text-green-900">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-green-600">Sammanfattning</p>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-green-600">{% trans "Sammanfattning" %}</p>
|
||||
<p class="text-lg font-semibold text-green-900">{{ claim.full_name }}</p>
|
||||
<p class="text-sm text-green-800">Belopp: <strong>{{ claim.amount }} {{ claim.currency }}</strong> · Konto: <span class="font-mono">{{ claim.account_number }}</span></p>
|
||||
<p class="text-sm text-green-800">{% trans "Belopp" %}: <strong>{{ claim.amount }} {{ claim.currency }}</strong> · {% trans "Konto" %}: <span class="font-mono">{{ claim.account_number }}</span></p>
|
||||
</div>
|
||||
{% if payments_enabled %}
|
||||
{% if claim.is_paid %}
|
||||
<p class="text-xs uppercase tracking-wide text-emerald-600">Markerad som betald</p>
|
||||
<p class="text-xs uppercase tracking-wide text-emerald-600">{% trans "Markerad som betald" %}</p>
|
||||
{% else %}
|
||||
<form method="post" onsubmit="return confirm('Är du säker på att du har lagt upp betalningen? Markera endast som betald om beloppet skickas till banken.');">
|
||||
<form method="post" onsubmit="return confirm('{% trans "Är du säker på att du har lagt upp betalningen? Markera endast som betald om beloppet skickas till banken." %}');">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action_type" value="payment">
|
||||
<input type="hidden" name="payment_claim_id" value="{{ claim.id }}">
|
||||
<button type="submit" class="inline-flex items-center gap-2 rounded-full bg-emerald-600 px-4 py-2 text-xs font-semibold uppercase tracking-wide text-white transition hover:bg-emerald-700">
|
||||
Betala
|
||||
{% trans "Betala" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-xs text-green-700">Intern betalningshantering är av – markera betalning i ekonomisystemet.</p>
|
||||
<p class="text-xs text-green-700">{% trans "Intern betalningshantering är av – markera betalning i ekonomisystemet." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,7 +105,7 @@
|
||||
|
||||
<div class="grid gap-6 px-6 py-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2">
|
||||
<p class="text-sm font-semibold text-gray-500">Beskrivning</p>
|
||||
<p class="text-sm font-semibold text-gray-500">{% trans "Beskrivning" %}</p>
|
||||
<p class="mt-2 whitespace-pre-wrap text-gray-800">{{ claim.description }}</p>
|
||||
<div class="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-600">
|
||||
{% if claim.receipt %}
|
||||
@@ -118,17 +113,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Visa kvitto
|
||||
{% trans "Visa kvitto" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-xs text-gray-400">Inget kvitto bifogat</span>
|
||||
<span class="text-xs text-gray-400">{% trans "Inget kvitto bifogat" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4 rounded-2xl bg-slate-50 p-5">
|
||||
<details class="group">
|
||||
<summary class="cursor-pointer select-none text-sm font-semibold text-gray-700">
|
||||
Logg & tidslinje
|
||||
{% trans "Logg & tidslinje" %}
|
||||
</summary>
|
||||
<ul class="mt-3 space-y-2 text-sm text-gray-600">
|
||||
{% for log in claim.logs.all %}
|
||||
@@ -136,17 +131,17 @@
|
||||
<p class="font-semibold text-gray-900">{{ log.get_action_display }}</p>
|
||||
<p class="text-xs text-gray-500">{{ log.created_at|date:"Y-m-d H:i" }}</p>
|
||||
{% if log.from_status %}
|
||||
<p class="text-xs text-gray-500">Status: {{ log.get_from_status_display }} → {{ log.get_to_status_display }}</p>
|
||||
<p class="text-xs text-gray-500">{% trans "Status" %}: {{ log.get_from_status_display }} → {{ log.get_to_status_display }}</p>
|
||||
{% endif %}
|
||||
{% if log.note %}
|
||||
<p class="mt-1 text-xs text-gray-600">"{{ log.note }}"</p>
|
||||
{% endif %}
|
||||
{% if log.performed_by %}
|
||||
<p class="text-xs text-gray-400">Av {{ log.performed_by.get_username }}</p>
|
||||
<p class="text-xs text-gray-400">{% trans "Av" %} {{ log.performed_by.get_username }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="text-xs text-gray-400">Ingen logg än.</li>
|
||||
<li class="text-xs text-gray-400">{% trans "Ingen logg än." %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
@@ -154,26 +149,26 @@
|
||||
{% if can_change %}
|
||||
{% if claim.is_paid %}
|
||||
<p class="rounded-lg bg-slate-100 px-3 py-2 text-xs text-slate-600">
|
||||
Utlägget är markerat som betalt. Ändringar av beslut/kommentar är låsta.
|
||||
{% trans "Utlägget är markerat som betalt. Ändringar av beslut/kommentar är låsta." %}
|
||||
</p>
|
||||
{% else %}
|
||||
<form method="post" class="space-y-3">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="claim_id" value="{{ claim.id }}">
|
||||
|
||||
<label class="block text-sm font-medium text-gray-700">Åtgärd</label>
|
||||
<select name="action" class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-brand-600">
|
||||
<label class="block text-sm font-medium text-gray-700">{% trans "Åtgärd" %}</label>
|
||||
<select name="action" 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">
|
||||
{% for value, label in decision_choices %}
|
||||
<option value="{{ value }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label class="block text-sm font-medium text-gray-700">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">{{ 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">
|
||||
Uppdatera beslut
|
||||
{% trans "Uppdatera beslut" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
@@ -185,8 +180,8 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border border-dashed border-gray-200 bg-white px-6 py-10 text-center text-gray-500">
|
||||
<p class="text-lg font-semibold text-gray-900">Inga utlägg ännu</p>
|
||||
<p class="mt-2 text-sm">När formuläret tas emot visas posterna automatiskt här.</p>
|
||||
<p class="text-lg font-semibold text-gray-900">{% trans "Inga utlägg ännu" %}</p>
|
||||
<p class="mt-2 text-sm">{% trans "När formuläret tas emot visas posterna automatiskt här." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="sv">
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<html lang="{{ LANGUAGE_CODE }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Claims{% endblock %}</title>
|
||||
<title>{% block title %}{% trans "Claims" %}{% endblock %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
@@ -24,37 +26,55 @@
|
||||
<body class="min-h-screen bg-slate-50 text-gray-900">
|
||||
<header class="bg-white shadow-sm">
|
||||
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-4">
|
||||
<a href="{% url 'claims:submit' %}" class="text-lg font-semibold text-gray-900 hover:text-brand-700">claims-system</a>
|
||||
<nav class="flex flex-wrap items-center gap-3 text-sm font-medium text-gray-600">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs uppercase tracking-wide text-gray-500">Offentlig</span>
|
||||
<a class="hover:text-gray-900" href="{% url 'claims:submit' %}">Skicka utlägg</a>
|
||||
</div>
|
||||
<a href="{% url 'claims:submit' %}" class="text-lg font-semibold text-gray-900 hover:text-brand-700">{% trans "claims-system" %}</a>
|
||||
<nav class="flex flex-wrap items-center gap-4 text-sm font-medium text-gray-600">
|
||||
<a class="rounded-full border border-gray-200 px-3 py-1 hover:text-gray-900" href="{% url 'claims:submit' %}">
|
||||
{% trans "Skicka utlägg" %}
|
||||
</a>
|
||||
{% if user.is_authenticated %}
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs uppercase tracking-wide text-gray-500">Intern</span>
|
||||
<a class="hover:text-gray-900" href="{% url 'claims:admin-list' %}">Utläggslista</a>
|
||||
<a class="hover:text-gray-900" href="{% url 'claims:my-claims' %}">Mina utlägg</a>
|
||||
{% if perms.auth.view_user %}
|
||||
<a class="hover:text-gray-900" href="{% url 'claims:user-manage' %}">Användare</a>
|
||||
{% endif %}
|
||||
<a class="hover:text-gray-900" href="{% url 'claims:export' %}">Export</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="hover:text-gray-900" href="{% url 'admin:index' %}">Django admin</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<details class="group relative">
|
||||
<summary class="flex cursor-pointer items-center gap-2 rounded-full border border-gray-200 px-3 py-1 text-sm text-gray-600 transition hover:text-gray-900">
|
||||
{% trans "Interna vyer" %}
|
||||
<svg class="h-3 w-3 transition group-open:rotate-180" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1.5L5 4.5L9 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<div class="absolute right-0 z-20 mt-2 w-52 rounded-2xl border border-gray-200 bg-white p-3 shadow-lg">
|
||||
<a class="block rounded-xl px-3 py-2 text-sm text-gray-700 hover:bg-gray-50" href="{% url 'claims:admin-list' %}">{% trans "Utläggslista" %}</a>
|
||||
<a class="block rounded-xl px-3 py-2 text-sm text-gray-700 hover:bg-gray-50" href="{% url 'claims:my-claims' %}">{% trans "Mina utlägg" %}</a>
|
||||
{% if perms.auth.view_user %}
|
||||
<a class="block rounded-xl px-3 py-2 text-sm text-gray-700 hover:bg-gray-50" href="{% url 'claims:user-manage' %}">{% trans "Användare" %}</a>
|
||||
{% endif %}
|
||||
<a class="block rounded-xl px-3 py-2 text-sm text-gray-700 hover:bg-gray-50" href="{% url 'claims:export' %}">{% trans "Export" %}</a>
|
||||
{% if user.is_staff %}
|
||||
<a class="block rounded-xl px-3 py-2 text-sm text-gray-700 hover:bg-gray-50" href="{% url 'admin:index' %}">{% trans "Django admin" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
<form action="{% url 'set_language' %}" method="post" class="inline-flex items-center gap-1 text-xs text-gray-500">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.path }}">
|
||||
<label for="lang-select" class="sr-only">{% trans "Språk" %}</label>
|
||||
<select id="lang-select" name="language" onchange="this.form.submit()" class="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs text-gray-700 focus:outline-none">
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
{% for code, name in LANGUAGES %}
|
||||
<option value="{{ code }}"{% if code == LANGUAGE_CODE %} selected{% endif %}>{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
{% if user.is_authenticated %}
|
||||
<span class="hidden text-xs text-gray-400 sm:inline">|</span>
|
||||
<span class="text-xs text-gray-500">Inloggad som {{ user.get_username }}</span>
|
||||
<span class="text-xs text-gray-500">{% trans "Inloggad som" %} {{ user.get_username }}</span>
|
||||
<form action="{% url 'logout' %}" method="post" class="inline">
|
||||
{% csrf_token %}
|
||||
<button class="rounded-full bg-gray-100 px-3 py-1 text-xs font-semibold text-gray-700 transition hover:bg-gray-200" type="submit">
|
||||
Logga ut
|
||||
{% trans "Logga ut" %}
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a class="rounded-full bg-brand-600 px-3 py-1 text-white transition hover:bg-brand-700" href="{% url 'login' %}">Logga in</a>
|
||||
<a class="rounded-full bg-brand-600 px-3 py-1 text-white transition hover:bg-brand-700" href="{% url 'login' %}">{% trans "Logga in" %}</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
{% load i18n %}
|
||||
{% extends "claims/base.html" %}
|
||||
|
||||
{% block title %}Export{% endblock %}
|
||||
{% block title %}{% trans "Export" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Export till redovisningssystem</h1>
|
||||
<p>Detta är ett framtida steg. Här kommer du att kunna:</p>
|
||||
<h1>{% trans "Export till redovisningssystem" %}</h1>
|
||||
<p>{% trans "Detta är ett framtida steg. Här kommer du att kunna:" %}</p>
|
||||
<ul>
|
||||
<li>Välja tidsperiod eller status</li>
|
||||
<li>Exportera till t.ex. bankfil eller SIE</li>
|
||||
<li>Skicka data via API till externa system</li>
|
||||
<li>{% trans "Välja tidsperiod eller status" %}</li>
|
||||
<li>{% trans "Exportera till t.ex. bankfil eller SIE" %}</li>
|
||||
<li>{% trans "Skicka data via API till externa system" %}</li>
|
||||
</ul>
|
||||
<p>Planerade åtgärder:</p>
|
||||
<p>{% trans "Planerade åtgärder:" %}</p>
|
||||
<ol>
|
||||
<li>Definiera format</li>
|
||||
<li>Implementera exportkommando/API</li>
|
||||
<li>Bygga integrationsinställningar</li>
|
||||
<li>{% trans "Definiera format" %}</li>
|
||||
<li>{% trans "Implementera exportkommando/API" %}</li>
|
||||
<li>{% trans "Bygga integrationsinställningar" %}</li>
|
||||
</ol>
|
||||
<p>Tills vidare kan du ladda ner data via Django admin eller med ett enkelt SQL-utdrag.</p>
|
||||
<p>{% trans "Tills vidare kan du ladda ner data via Django admin eller med ett enkelt SQL-utdrag." %}</p>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
{% load i18n %}
|
||||
{{ formset.management_form }}
|
||||
|
||||
<div class="space-y-8" data-formset-list>
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">Steg 2</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">Utläggsrader</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">Lägg till ett block per kvitto eller kostnad. Projektväljaren hjälper ekonomin att bokföra rätt.</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Steg 2" %}</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">{% trans "Utläggsrader" %}</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">{% trans "Lägg till ett block per kvitto eller kostnad. Projektväljaren hjälper ekonomin att bokföra rätt." %}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="rounded-full bg-gray-200 px-4 py-1 text-sm text-gray-700">
|
||||
Totalt <span data-current-count>{{ current_forms }}</span> rader
|
||||
{% blocktrans %}Totalt <span data-current-count>{{ current_forms }}</span> rader{% endblocktrans %}
|
||||
</span>
|
||||
<div class="flex overflow-hidden rounded-full border border-gray-200 bg-white shadow-sm">
|
||||
<button type="button"
|
||||
@@ -18,7 +19,7 @@
|
||||
{% if not can_remove_forms %}disabled{% endif %}>
|
||||
–
|
||||
</button>
|
||||
<div class="border-l border-r border-gray-200 px-3 py-2 text-sm text-gray-500">justera</div>
|
||||
<div class="border-l border-r border-gray-200 px-3 py-2 text-sm text-gray-500">{% trans "justera" %}</div>
|
||||
<button type="button"
|
||||
class="px-3 py-2 text-sm font-semibold text-gray-600 transition hover:bg-gray-50 {% if not can_add_forms %}pointer-events-none opacity-40{% endif %}"
|
||||
data-action="add-form"
|
||||
@@ -32,16 +33,16 @@
|
||||
{% for form in formset %}
|
||||
<div class="rounded-2xl bg-white shadow-sm ring-1 ring-gray-100" data-claim-card>
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Utlägg {{ forloop.counter }}</h3>
|
||||
<p class="text-xs text-gray-500">Obligatoriska fält markeras med *</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900">{% blocktrans %}Utlägg {{ forloop.counter }}{% endblocktrans %}</h3>
|
||||
<p class="text-xs text-gray-500">{% trans "Obligatoriska fält markeras med *" %}</p>
|
||||
</div>
|
||||
<div class="space-y-6 px-6 py-6">
|
||||
{{ form.non_field_errors }}
|
||||
{% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700">
|
||||
{{ form.description.label }}<span class="text-rose-500"> *</span>
|
||||
<label class="text-sm font-medium text-gray-700">
|
||||
{{ form.description.label }}<span class="text-rose-500"> *</span>
|
||||
</label>
|
||||
{{ form.description }}
|
||||
{% for error in form.description.errors %}
|
||||
@@ -72,7 +73,7 @@
|
||||
|
||||
<details class="rounded-xl border border-dashed border-gray-200 px-4 py-3 text-sm text-gray-600">
|
||||
<summary class="cursor-pointer select-none text-base font-medium text-gray-800">
|
||||
Avancerat: justera valuta (standard SEK)
|
||||
{% trans "Avancerat: justera valuta (standard SEK)" %}
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
@@ -82,7 +83,7 @@
|
||||
<p class="mt-1 text-sm text-rose-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">Använd detta om kvittot är i annan valuta än svenska kronor.</p>
|
||||
<p class="text-xs text-gray-500">{% trans "Använd detta om kvittot är i annan valuta än svenska kronor." %}</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -100,11 +101,11 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-2xl bg-white p-6 shadow-sm ring-1 ring-gray-100">
|
||||
<p class="text-sm text-gray-600">
|
||||
När du skickar in skickas du vidare mot adminvyn. Saknar du inloggning får du möjlighet att logga in.
|
||||
<p class="text-sm text-gray-600">
|
||||
{% trans "När du skickar in skickas du vidare mot adminvyn. Saknar du inloggning får du möjlighet att logga in." %}
|
||||
</p>
|
||||
<button type="submit" class="inline-flex items-center gap-2 rounded-full bg-brand-600 px-6 py-3 text-sm font-semibold text-white transition hover:bg-brand-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600 focus-visible:ring-offset-2">
|
||||
Skicka in utlägg
|
||||
{% trans "Skicka in utlägg" %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17.25 8.25L21 12m0 0-3.75 3.75M21 12H3" />
|
||||
</svg>
|
||||
@@ -115,8 +116,8 @@
|
||||
<template id="claim-line-template">
|
||||
<div class="rounded-2xl bg-white shadow-sm ring-1 ring-gray-100" data-claim-card>
|
||||
<div class="border-b border-gray-100 px-6 py-4 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Ny utläggsrad</h3>
|
||||
<p class="text-xs text-gray-500">Obligatoriska fält markeras med *</p>
|
||||
<h3 class="text-lg font-semibold text-gray-900">{% trans "Ny utläggsrad" %}</h3>
|
||||
<p class="text-xs text-gray-500">{% trans "Obligatoriska fält markeras med *" %}</p>
|
||||
</div>
|
||||
<div class="space-y-6 px-6 py-6">
|
||||
{{ empty_form.non_field_errors }}
|
||||
@@ -146,21 +147,21 @@
|
||||
|
||||
<details class="rounded-xl border border-dashed border-gray-200 px-4 py-3 text-sm text-gray-600">
|
||||
<summary class="cursor-pointer select-none text-base font-medium text-gray-800">
|
||||
Avancerat: justera valuta (standard SEK)
|
||||
{% trans "Avancerat: justera valuta (standard SEK)" %}
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700">{{ empty_form.currency.label }}</label>
|
||||
{{ empty_form.currency }}
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">Använd detta om kvittot är i annan valuta än svenska kronor.</p>
|
||||
<p class="text-xs text-gray-500">{% trans "Använd detta om kvittot är i annan valuta än svenska kronor." %}</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700">{{ empty_form.receipt.label }}</label>
|
||||
{{ empty_form.receipt }}
|
||||
<p class="mt-1 text-xs text-gray-500">PDF, JPG eller PNG – max 10 MB.</p>
|
||||
<p class="mt-1 text-xs text-gray-500">{% trans "PDF, JPG eller PNG – max 10 MB." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
{% extends "claims/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Mina utlägg{% endblock %}
|
||||
{% block title %}{% trans "Mina utlägg" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="space-y-6 py-6">
|
||||
<header class="max-w-3xl">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">Översikt</p>
|
||||
<h1 class="text-3xl font-semibold text-gray-900">Mina utlägg</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Här ser du alla utlägg du skickat in när du varit inloggad, inklusive status, kvitton och loggar.</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">{% trans "Översikt" %}</p>
|
||||
<h1 class="text-3xl font-semibold text-gray-900">{% trans "Mina utlägg" %}</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">{% trans "Här ser du status för de utlägg du skickat in när du varit inloggad." %}</p>
|
||||
</header>
|
||||
|
||||
{% if claims %}
|
||||
@@ -16,11 +17,11 @@
|
||||
<article class="rounded-3xl bg-white px-6 py-6 shadow-sm ring-1 ring-gray-100">
|
||||
<div class="flex flex-col gap-4 border-b border-gray-100 pb-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500">Skickad {{ claim.created_at|date:"Y-m-d H:i" }}</p>
|
||||
<h2 class="mt-1 text-2xl font-semibold text-gray-900">{{ claim.description|default:"Utlägg" }}</h2>
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500">{% trans "Skickad" %} {{ claim.created_at|date:"Y-m-d H:i" }}</p>
|
||||
<h2 class="mt-1 text-2xl font-semibold text-gray-900">{{ claim.description|default:_("Utlägg") }}</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Belopp: <strong>{{ claim.amount }} {{ claim.currency }}</strong><br>
|
||||
Projekt: {{ claim.project|default:"-" }}
|
||||
{% trans "Belopp" %}: <strong>{{ claim.amount }} {{ claim.currency }}</strong><br>
|
||||
{% trans "Projekt" %}: {{ claim.project|default:"-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-2 text-sm lg:items-end">
|
||||
@@ -29,43 +30,43 @@
|
||||
</span>
|
||||
{% if claim.paid_at %}
|
||||
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-800">
|
||||
Betald {{ claim.paid_at|date:"Y-m-d" }}
|
||||
{% trans "Betald" %} {{ claim.paid_at|date:"Y-m-d" }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 grid gap-4 lg:grid-cols-[2fr,1fr]">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-gray-500">Detaljer</p>
|
||||
<p class="text-sm font-semibold text-gray-500">{% trans "Detaljer" %}</p>
|
||||
<p class="mt-2 whitespace-pre-wrap text-gray-800">{{ claim.description }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl bg-slate-50 p-4 text-sm text-gray-600">
|
||||
<p class="font-semibold text-gray-800">Kvitto</p>
|
||||
<p class="font-semibold text-gray-800">{% trans "Kvitto" %}</p>
|
||||
{% if claim.receipt %}
|
||||
<a class="mt-2 inline-flex items-center gap-2 text-brand-600 hover:text-brand-700" href="{{ claim.receipt.url }}" target="_blank" rel="noopener">
|
||||
Visa fil
|
||||
{% trans "Visa fil" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="mt-2 text-xs text-gray-400">Inget kvitto bifogat.</p>
|
||||
<p class="mt-2 text-xs text-gray-400">{% trans "Inget kvitto bifogat." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<details class="mt-4 rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-4 text-sm text-gray-700">
|
||||
<summary class="cursor-pointer select-none text-sm font-semibold text-gray-800">Visa logg</summary>
|
||||
<summary class="cursor-pointer select-none text-sm font-semibold text-gray-800">{% trans "Visa logg" %}</summary>
|
||||
<ul class="mt-3 space-y-2 text-sm text-gray-600">
|
||||
{% for log in claim.logs.all %}
|
||||
<li class="rounded-lg bg-white px-3 py-2 shadow-sm">
|
||||
<p class="font-semibold text-gray-900">{{ log.get_action_display }}</p>
|
||||
<p class="text-xs text-gray-500">{{ log.created_at|date:"Y-m-d H:i" }}</p>
|
||||
{% if log.from_status %}
|
||||
<p class="text-xs text-gray-500">Status: {{ log.get_from_status_display }} → {{ log.get_to_status_display }}</p>
|
||||
<p class="text-xs text-gray-500">{% trans "Status" %}: {{ log.get_from_status_display }} → {{ log.get_to_status_display }}</p>
|
||||
{% endif %}
|
||||
{% if log.note %}
|
||||
<p class="mt-1 text-xs text-gray-600">"{{ log.note }}"</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="text-xs text-gray-400">Ingen logg än.</li>
|
||||
<li class="text-xs text-gray-400">{% trans "Ingen logg än." %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
@@ -74,8 +75,8 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-3xl border border-dashed border-gray-200 bg-white px-6 py-10 text-center text-gray-500">
|
||||
<p class="text-lg font-semibold text-gray-900">Inga utlägg ännu</p>
|
||||
<p class="mt-2 text-sm">När du skickar in utlägg medan du är inloggad dyker de upp här.</p>
|
||||
<p class="text-lg font-semibold text-gray-900">{% trans "Inga utlägg ännu" %}</p>
|
||||
<p class="mt-2 text-sm">{% trans "Du har inte skickat in några utlägg ännu eller så gjordes de utan inloggning." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
{% extends "claims/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Skicka utlägg{% endblock %}
|
||||
{% block title %}{% trans "Skicka utlägg" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="py-8">
|
||||
<div class="mx-auto max-w-4xl text-center">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">Utlägg</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">Skicka in dina kostnader</h1>
|
||||
<p class="mt-3 text-base text-gray-600">
|
||||
Formuläret fungerar både för inloggade och gäster. Varje rad nedan motsvarar ett utlägg.
|
||||
Behöver du fler rader? Lägg till <code class="rounded bg-gray-100 px-2 py-1 text-xs">?forms=n</code> i URL:en (max {{ max_extra_forms }}).
|
||||
</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">{% trans "Utlägg" %}</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">{% trans "Skicka in dina kostnader" %}</h1>
|
||||
</div>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="mx-auto mt-10 max-w-4xl space-y-10">
|
||||
@@ -18,9 +15,9 @@
|
||||
|
||||
<div class="rounded-2xl bg-white shadow-sm ring-1 ring-gray-100">
|
||||
<div class="border-b border-gray-100 px-6 py-5">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">Steg 1</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">Dina uppgifter</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">Vi återkommer via dessa kontaktuppgifter och använder kontonumret för utbetalning.</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Steg 1" %}</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">{% trans "Dina uppgifter" %}</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">{% trans "Vi återkommer via dessa kontaktuppgifter och använder kontonumret för utbetalning." %}</p>
|
||||
</div>
|
||||
<div class="space-y-6 px-6 py-6">
|
||||
{% for field in claimant_form %}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
{% extends "claims/base.html" %}
|
||||
|
||||
{% block title %}Tack för ditt utlägg{% endblock %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Tack för ditt utlägg" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex min-h-[60vh] items-center justify-center py-10">
|
||||
<div class="w-full max-w-lg rounded-3xl bg-white px-8 py-10 text-center shadow-lg ring-1 ring-gray-100">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">Tack!</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">Utlägget är skickat</h1>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">{% trans "Tack!" %}</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">{% trans "Utlägget är skickat" %}</h1>
|
||||
<p class="mt-3 text-sm text-gray-600">
|
||||
Vi har tagit emot underlaget. Om du har fler kvitton kan du fylla i ett nytt formulär direkt,
|
||||
annars kan du logga in för att följa statusen.
|
||||
{% trans "Vi har tagit emot underlaget. Om du har fler kvitton kan du fylla i ett nytt formulär direkt, annars kan du logga in för att följa statusen." %}
|
||||
</p>
|
||||
<div class="mt-6 flex flex-col gap-3 sm:flex-row sm:justify-center">
|
||||
<a href="{% url 'claims:submit' %}"
|
||||
class="inline-flex items-center justify-center rounded-full bg-brand-600 px-5 py-3 text-sm font-semibold text-white transition hover:bg-brand-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600 focus-visible:ring-offset-2">
|
||||
Skicka nytt utlägg
|
||||
{% trans "Skicka nytt utlägg" %}
|
||||
</a>
|
||||
<a href="{% url 'login' %}"
|
||||
class="inline-flex items-center justify-center rounded-full border border-gray-200 px-5 py-3 text-sm font-semibold text-gray-700 transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600 focus-visible:ring-offset-2">
|
||||
Logga in
|
||||
{% trans "Logga in" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
{% extends "claims/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Användarhantering{% endblock %}
|
||||
{% block title %}{% trans "Användarhantering" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="space-y-10 py-8">
|
||||
<header class="max-w-3xl">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">Konton & behörigheter</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">Hantera användare</h1>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-brand-600">{% trans "Konton & behörigheter" %}</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold text-gray-900">{% trans "Hantera användare" %}</h1>
|
||||
<p class="mt-3 text-sm text-gray-600">
|
||||
Skapa nya konton, justera rättigheter för claim-flödet och ta bort användare som inte längre ska ha åtkomst.
|
||||
{% trans "Skapa nya konton, justera rättigheter för claim-flödet och ta bort användare som inte längre ska ha åtkomst." %}
|
||||
</p>
|
||||
<div class="mt-4 rounded-2xl border border-dashed border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-800">
|
||||
Notis: denna sida styr direkta behörigheter. Rättigheter via grupper eller superuser-status gäller även om kryssrutorna avmarkeras.
|
||||
{% trans "Notis: denna sida styr direkta behörigheter. Rättigheter via grupper eller superuser-status gäller även om kryssrutorna avmarkeras." %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-8 lg:grid-cols-2">
|
||||
<div class="rounded-3xl bg-white px-6 py-8 shadow-sm ring-1 ring-gray-100">
|
||||
<div class="border-b border-gray-100 pb-4">
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">Nytt konto</p>
|
||||
<h2 class="mt-1 text-2xl font-semibold text-gray-900">Skapa användare</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">Lösenordet valideras mot Djangos standardregler.</p>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Nytt konto" %}</p>
|
||||
<h2 class="mt-1 text-2xl font-semibold text-gray-900">{% trans "Skapa användare" %}</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">{% trans "Lösenordet valideras mot Djangos standardregler." %}</p>
|
||||
</div>
|
||||
<form method="post" class="mt-6 space-y-4">
|
||||
{% csrf_token %}
|
||||
@@ -38,8 +39,6 @@
|
||||
value="{{ field.value|default_if_none:'' }}"
|
||||
class="mt-1 block w-full rounded-xl border border-gray-200 px-3 py-2 text-sm shadow-sm focus:border-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-600"
|
||||
{% if field.field.required %}required{% endif %}
|
||||
{% if field.field.widget.attrs.autocomplete %}autocomplete="{{ field.field.widget.attrs.autocomplete }}"{% endif %}
|
||||
{% if field.field.widget.attrs.autofocus %}autofocus{% endif %}
|
||||
/>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
@@ -51,33 +50,33 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit" class="w-full rounded-2xl bg-brand-600 px-4 py-3 text-sm font-semibold text-white transition hover:bg-brand-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-600 focus-visible:ring-offset-2">
|
||||
Skapa användare
|
||||
{% trans "Skapa användare" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="rounded-3xl bg-slate-900 px-6 py-8 text-slate-100 shadow-sm ring-1 ring-slate-800">
|
||||
<h3 class="text-2xl font-semibold">Tips för kontohantering</h3>
|
||||
<ul class="mt-4 space-y-3 text-sm text-slate-300 break-words">
|
||||
<h3 class="text-2xl font-semibold">{% trans "Tips för kontohantering" %}</h3>
|
||||
<ul class="mt-4 space-y-3 text-sm text-slate-300">
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1 h-2 w-2 rounded-full bg-brand-400"></span>
|
||||
<span class="min-w-0">Lägg användare i grupper via Django admin om flera personer ska dela samma roll.</span>
|
||||
<span class="min-w-0">{% trans "Lägg användare i grupper via Django admin om flera personer ska dela samma roll." %}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1 h-2 w-2 rounded-full bg-brand-400"></span>
|
||||
<span class="min-w-0">
|
||||
Behörigheterna <code class="break-normal rounded bg-slate-800 px-2 py-1 text-xs">claims.view_claim</code>
|
||||
{% blocktrans %}Behörigheterna <code class="break-normal rounded bg-slate-800 px-2 py-1 text-xs">claims.view_claim</code>
|
||||
och <code class="break-normal rounded bg-slate-800 px-2 py-1 text-xs">claims.change_claim</code>
|
||||
styr åtkomst till adminvyn respektive beslutsflödet.
|
||||
styr åtkomst till adminvyn respektive beslutsflödet.{% endblocktrans %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1 h-2 w-2 rounded-full bg-brand-400"></span>
|
||||
<span class="min-w-0">En markerad <strong>Admin/staff</strong>-användare kan nå Django admin och skapa projekt, exportflöden m.m.</span>
|
||||
<span class="min-w-0">{% trans "En markerad Admin/staff-användare kan nå Django admin och skapa projekt, exportflöden m.m." %}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1 h-2 w-2 rounded-full bg-brand-400"></span>
|
||||
<span class="min-w-0">Ta bara bort konton du är säker på – historik försvinner inte, men personen tappar all åtkomst.</span>
|
||||
<span class="min-w-0">{% trans "Ta bara bort konton du är säker på – historik försvinner inte, men personen tappar all åtkomst." %}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -85,8 +84,8 @@
|
||||
|
||||
<section class="space-y-6">
|
||||
<div>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">Befintliga användare</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">Justera behörigheter</h2>
|
||||
<p class="text-sm font-semibold uppercase tracking-wide text-gray-500">{% trans "Befintliga användare" %}</p>
|
||||
<h2 class="text-2xl font-semibold text-gray-900">{% trans "Justera behörigheter" %}</h2>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{% for row in user_rows %}
|
||||
@@ -97,69 +96,52 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-xl font-semibold text-gray-900">{{ user.username }}</h3>
|
||||
{% if user.is_superuser %}
|
||||
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700">Superuser</span>
|
||||
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-emerald-700">{% trans "Superuser" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ user.get_full_name|default:"Saknar namn" }} · {{ user.email|default:"Ingen e-post" }}
|
||||
{{ user.get_full_name|default:_("Saknar namn") }} · {{ user.email|default:_("Ingen e-post") }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs uppercase tracking-wide text-gray-400">
|
||||
ID: {{ user.id }}
|
||||
</p>
|
||||
<p class="text-xs uppercase tracking-wide text-gray-400">ID: {{ user.id }}</p>
|
||||
</div>
|
||||
<div class="mt-4 grid gap-6 lg:grid-cols-[2fr,1fr]">
|
||||
<form method="post" class="space-y-4 rounded-2xl bg-slate-50 p-4">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="update">
|
||||
{{ form.user_id }}
|
||||
<div class="space-y-3 text-sm text-gray-700">
|
||||
<label class="flex items-center gap-2" for="{{ form.is_staff.id_for_label }}">
|
||||
<input type="checkbox"
|
||||
id="{{ form.is_staff.id_for_label }}"
|
||||
name="{{ form.is_staff.html_name }}"
|
||||
value="on"
|
||||
{% if form.is_staff.value %}checked{% endif %}
|
||||
class="h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-600">
|
||||
<span>Admin/staff</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2" for="{{ form.grant_view.id_for_label }}">
|
||||
<input type="checkbox"
|
||||
id="{{ form.grant_view.id_for_label }}"
|
||||
name="{{ form.grant_view.html_name }}"
|
||||
value="on"
|
||||
{% if form.grant_view.value %}checked{% endif %}
|
||||
class="h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-600">
|
||||
<span>Får se utlägg</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2" for="{{ form.grant_change.id_for_label }}">
|
||||
<input type="checkbox"
|
||||
id="{{ form.grant_change.id_for_label }}"
|
||||
name="{{ form.grant_change.html_name }}"
|
||||
value="on"
|
||||
{% if form.grant_change.value %}checked{% endif %}
|
||||
class="h-4 w-4 rounded border-gray-300 text-brand-600 focus:ring-brand-600">
|
||||
<span>Får besluta utlägg</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-gray-700">
|
||||
<label class="flex items-center gap-2" for="{{ form.is_staff.id_for_label }}">
|
||||
{{ form.is_staff }}
|
||||
<span>{% trans "Admin/staff" %}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2" for="{{ form.grant_view.id_for_label }}">
|
||||
{{ form.grant_view }}
|
||||
<span>{% trans "Får se utlägg" %}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2" for="{{ form.grant_change.id_for_label }}">
|
||||
{{ form.grant_change }}
|
||||
<span>{% trans "Får besluta utlägg" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="w-full rounded-2xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700">
|
||||
Spara behörigheter
|
||||
{% trans "Spara behörigheter" %}
|
||||
</button>
|
||||
</form>
|
||||
<div class="rounded-2xl border border-red-100 bg-red-50 p-4 text-sm text-red-800">
|
||||
<p class="font-semibold">Ta bort konto</p>
|
||||
<p class="font-semibold">{% trans "Ta bort konto" %}</p>
|
||||
{% if delete_form %}
|
||||
<p class="mt-1 text-xs text-red-700">Åtgärden går inte att ångra. Användaren förlorar omedelbart åtkomst.</p>
|
||||
<form method="post" class="mt-4" onsubmit="return confirm('Ta bort {{ user.username }}?');">
|
||||
<p class="mt-1 text-xs text-red-700">{% trans "Åtgärden går inte att ångra. Användaren förlorar omedelbart åtkomst." %}</p>
|
||||
<form method="post" class="mt-4" onsubmit="return confirm('{% blocktrans %}Ta bort {{ user.username }}?{% endblocktrans %}');">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="delete">
|
||||
{{ delete_form.user_id }}
|
||||
<button type="submit" class="w-full rounded-full bg-red-600 px-4 py-2 text-xs font-semibold text-white transition hover:bg-red-700">
|
||||
Ta bort användare
|
||||
{% trans "Ta bort användare" %}
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="mt-2 text-xs text-red-700">Kan inte tas bort (antingen du själv eller superuser).</p>
|
||||
<p class="mt-2 text-xs text-red-700">{% trans "Kan inte tas bort (antingen du själv eller superuser)." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,8 +149,8 @@
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
<div class="rounded-3xl border border-dashed border-gray-200 bg-white px-6 py-10 text-center text-gray-500">
|
||||
<p class="text-lg font-semibold text-gray-900">Inga användare ännu</p>
|
||||
<p class="mt-2 text-sm">Skapa det första kontot via formuläret ovan.</p>
|
||||
<p class="text-lg font-semibold text-gray-900">{% trans "Inga användare upplagda." %}</p>
|
||||
<p class="mt-2 text-sm">{% trans "Skapa det första kontot via formuläret ovan." %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
8
claims/templates/emails/claim_submitted_admin.html
Normal file
8
claims/templates/emails/claim_submitted_admin.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<h2>Nytt utlägg inskickat</h2>
|
||||
<ul>
|
||||
<li><strong>Person:</strong> {{ claim.full_name }} ({{ claim.email }})</li>
|
||||
<li><strong>Belopp:</strong> {{ claim.amount }} {{ claim.currency }}</li>
|
||||
<li><strong>Projekt:</strong> {{ claim.project|default:"-" }}</li>
|
||||
<li><strong>Beskrivning:</strong> {{ claim.description }}</li>
|
||||
</ul>
|
||||
<p>Logga in för att granska och godkänna.</p>
|
||||
9
claims/templates/emails/claim_submitted_claimant.html
Normal file
9
claims/templates/emails/claim_submitted_claimant.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<h2>Hej {{ claim.full_name }},</h2>
|
||||
<p>Tack för att du skickade in ditt utlägg. Vi har mottagit följande information:</p>
|
||||
<ul>
|
||||
<li><strong>Belopp:</strong> {{ claim.amount }} {{ claim.currency }}</li>
|
||||
<li><strong>Projekt:</strong> {{ claim.project|default:"-" }}</li>
|
||||
<li><strong>Beskrivning:</strong> {{ claim.description }}</li>
|
||||
</ul>
|
||||
<p>Du får en ny avisering när ett beslut har fattats.</p>
|
||||
<p>Vänliga hälsningar,<br>claims-system</p>
|
||||
@@ -7,6 +7,7 @@ from django.forms import formset_factory
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
from django.views.generic import ListView, TemplateView
|
||||
|
||||
@@ -18,6 +19,7 @@ from .forms import (
|
||||
UserManagementForm,
|
||||
UserPermissionForm,
|
||||
)
|
||||
from .email_utils import notify_admin_of_claim, send_claimant_confirmation_email
|
||||
from .models import Claim, ClaimLog, SystemSetting
|
||||
|
||||
User = get_user_model()
|
||||
@@ -108,15 +110,17 @@ class SubmitClaimView(View):
|
||||
performed_by=claim.submitted_by,
|
||||
to_status=Claim.Status.PENDING,
|
||||
)
|
||||
send_claimant_confirmation_email(claim)
|
||||
notify_admin_of_claim(claim)
|
||||
created += 1
|
||||
|
||||
if created:
|
||||
messages.success(request, f"{created} utlägg skickade in.")
|
||||
messages.success(request, _("{} utlägg skickade in.").format(created))
|
||||
return redirect(reverse("claims:submit-success"))
|
||||
|
||||
messages.error(request, "Inga utlägg kunde sparas. Fyll i minst en rad.")
|
||||
messages.error(request, _("Inga utlägg kunde sparas. Fyll i minst en rad."))
|
||||
else:
|
||||
messages.error(request, "Kunde inte spara utläggen. Kontrollera formuläret.")
|
||||
messages.error(request, _("Kunde inte spara utläggen. Kontrollera formuläret."))
|
||||
|
||||
return render(request, self.template_name, self.build_context(formset, claimant_form))
|
||||
|
||||
@@ -161,7 +165,7 @@ class ClaimAdminListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
|
||||
def _handle_decision(self, request):
|
||||
if not request.user.has_perm("claims.change_claim"):
|
||||
messages.error(request, "Du har inte behörighet att uppdatera utlägg.")
|
||||
messages.error(request, _("Du har inte behörighet att uppdatera utlägg."))
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
form = ClaimDecisionForm(request.POST)
|
||||
@@ -175,17 +179,17 @@ class ClaimAdminListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
action = form.cleaned_data["action"]
|
||||
decision_note = form.cleaned_data.get("decision_note", "")
|
||||
if claim.is_paid:
|
||||
messages.error(request, "Utlägget är redan markerat som betalt och kan inte ändras.")
|
||||
messages.error(request, _("Utlägget är redan markerat som betalt och kan inte ändras."))
|
||||
return redirect(request.get_full_path())
|
||||
previous_status = claim.status
|
||||
claim.decision_note = decision_note
|
||||
|
||||
if action == ClaimDecisionForm.ACTION_APPROVE:
|
||||
claim.status = Claim.Status.APPROVED
|
||||
messages.success(request, f"{claim} markerades som godkänd.")
|
||||
messages.success(request, _("%(claim)s markerades som godkänd.") % {"claim": claim})
|
||||
else:
|
||||
claim.status = Claim.Status.REJECTED
|
||||
messages.warning(request, f"{claim} markerades som nekad.")
|
||||
messages.warning(request, _("%(claim)s markerades som nekad.") % {"claim": claim})
|
||||
|
||||
claim.save(update_fields=["status", "decision_note", "updated_at"])
|
||||
claim.add_log(
|
||||
@@ -199,18 +203,18 @@ class ClaimAdminListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
|
||||
def _handle_payment(self, request):
|
||||
if not SystemSetting.internal_payments_active():
|
||||
messages.error(request, "Betalningshantering är inte aktiverad.")
|
||||
messages.error(request, _("Betalningshantering är inte aktiverad."))
|
||||
return redirect(request.get_full_path())
|
||||
if not request.user.has_perm("claims.change_claim"):
|
||||
messages.error(request, "Du har inte behörighet att uppdatera utlägg.")
|
||||
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("payment_claim_id"))
|
||||
if claim.status != Claim.Status.APPROVED:
|
||||
messages.error(request, "Endast godkända utlägg kan markeras som betalda.")
|
||||
messages.error(request, _("Endast godkända utlägg kan markeras som betalda."))
|
||||
return redirect(request.get_full_path())
|
||||
if claim.is_paid:
|
||||
messages.info(request, "Detta utlägg är redan markerat som betalt.")
|
||||
messages.info(request, _("Detta utlägg är redan markerat som betalt."))
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
claim.paid_by = request.user
|
||||
@@ -221,7 +225,7 @@ class ClaimAdminListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
performed_by=request.user,
|
||||
note="Markerad som betald via systemet.",
|
||||
)
|
||||
messages.success(request, f"{claim} markerades som betald.")
|
||||
messages.success(request, _("%(claim)s markerades som betald.") % {"claim": claim})
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
|
||||
@@ -250,7 +254,7 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
def _ensure_perm(self, perm_codename):
|
||||
perm = f"auth.{perm_codename}"
|
||||
if not self.request.user.has_perm(perm):
|
||||
messages.error(self.request, "Du saknar behörighet för åtgärden.")
|
||||
messages.error(self.request, _("Du saknar behörighet för åtgärden."))
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -297,7 +301,7 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
)
|
||||
self._set_perm(user, "claims.view_claim", form.cleaned_data.get("grant_view", False))
|
||||
self._set_perm(user, "claims.change_claim", form.cleaned_data.get("grant_change", False))
|
||||
messages.success(request, f"Användaren {user.username} skapades.")
|
||||
messages.success(request, _("Användaren %(user)s skapades.") % {"user": user.username})
|
||||
return redirect(request.path)
|
||||
return self.render_to_response(self.get_context_data(create_form=form))
|
||||
|
||||
@@ -308,15 +312,15 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
if form.is_valid():
|
||||
user = get_object_or_404(User, pk=form.cleaned_data["user_id"])
|
||||
if user == request.user and not form.cleaned_data["is_staff"]:
|
||||
messages.error(request, "Du kan inte ta bort din egen staff-status.")
|
||||
messages.error(request, _("Du kan inte ta bort din egen staff-status."))
|
||||
return redirect(request.path)
|
||||
user.is_staff = form.cleaned_data["is_staff"]
|
||||
user.save(update_fields=["is_staff"])
|
||||
self._set_perm(user, "claims.view_claim", form.cleaned_data["grant_view"])
|
||||
self._set_perm(user, "claims.change_claim", form.cleaned_data["grant_change"])
|
||||
messages.success(request, f"Behörigheter uppdaterades för {user.username}.")
|
||||
messages.success(request, _("Behörigheter uppdaterades för %(user)s.") % {"user": user.username})
|
||||
else:
|
||||
messages.error(request, "Kunde inte uppdatera behörigheter.")
|
||||
messages.error(request, _("Kunde inte uppdatera behörigheter."))
|
||||
return redirect(request.path)
|
||||
|
||||
elif action == "delete":
|
||||
@@ -326,15 +330,15 @@ class UserManagementView(LoginRequiredMixin, PermissionRequiredMixin, TemplateVi
|
||||
if form.is_valid():
|
||||
user = get_object_or_404(User, pk=form.cleaned_data["user_id"])
|
||||
if user == request.user:
|
||||
messages.error(request, "Du kan inte ta bort ditt eget konto.")
|
||||
messages.error(request, _("Du kan inte ta bort ditt eget konto."))
|
||||
elif user.is_superuser:
|
||||
messages.error(request, "Du kan inte ta bort en superuser via detta gränssnitt.")
|
||||
messages.error(request, _("Du kan inte ta bort en superuser via detta gränssnitt."))
|
||||
else:
|
||||
user.delete()
|
||||
messages.warning(request, "Användaren togs bort.")
|
||||
messages.warning(request, _("Användaren togs bort."))
|
||||
return redirect(request.path)
|
||||
|
||||
messages.error(request, "Okänd åtgärd.")
|
||||
messages.error(request, _("Okänd åtgärd."))
|
||||
return redirect(request.path)
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user