Files
Inventory/inventory/forms.py
2026-09-29 08:43:05 +02:00

314 lines
15 KiB
Python

from django import forms
from copy import deepcopy
from django.db import models
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
from .models import ChoiceValue, Device, Employee, FieldDefinition, FieldOption, UserProfile
from .device_types import HARDWARE_FIELDS, applicable_fields
GERMAN_DATE_INPUT_FORMATS = ["%d.%m.%Y", "%Y-%m-%d"]
GERMAN_DATE_WIDGET = {"type": "text", "placeholder": "TT.MM.JJJJ", "inputmode": "numeric", "data-german-date": ""}
STATUS_COLOR_CHOICES = [
("#2563eb", "Blau"), ("#0891b2", "Türkis"), ("#15803d", "Grün"),
("#4d7c0f", "Limette"), ("#b45309", "Orange"), ("#a16207", "Gelb"),
("#dc2626", "Rot"), ("#be185d", "Pink"), ("#7c3aed", "Violett"),
("#4f46e5", "Indigo"), ("#64748b", "Grau"), ("#334155", "Anthrazit"),
]
class QuickAddForm(forms.Form):
number = forms.IntegerField(label="Erste Nr.", min_value=1, max_value=2147483647, help_text="Die vorgeschlagene Startnummer kann geändert werden.")
quantity = forms.IntegerField(label="Anzahl", min_value=1, initial=1, help_text="Alle Geräte erhalten aufsteigende Nummern.")
shelf = forms.CharField(label="Regal", max_length=80, required=False)
status = forms.ModelChoiceField(label="Status", queryset=ChoiceValue.objects.none())
date = forms.DateField(
label="Datum", input_formats=GERMAN_DATE_INPUT_FORMATS,
widget=forms.DateInput(format="%d.%m.%Y", attrs=GERMAN_DATE_WIDGET),
)
device_type = forms.ModelChoiceField(label="Typ", queryset=ChoiceValue.objects.none(), required=False)
employee = forms.ModelChoiceField(
label="Bearbeiter", queryset=Employee.objects.none(), required=True,
empty_label="Bearbeiter auswählen", error_messages={"required": "Bitte einen Bearbeiter auswählen."},
)
donor = forms.CharField(label="Spender", max_length=200, required=False)
model = forms.CharField(label="Modell", max_length=200, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["status"].queryset = ChoiceValue.objects.filter(kind=ChoiceValue.Kind.STATUS, active=True)
self.fields["device_type"].queryset = ChoiceValue.objects.filter(kind=ChoiceValue.Kind.DEVICE_TYPE, active=True)
self.fields["employee"].queryset = Employee.objects.filter(active=True)
selected_type = None
if self.is_bound and str(self.data.get("device_type", "")).isdecimal():
selected_type = self.fields["device_type"].queryset.filter(pk=self.data["device_type"]).first()
for name in sorted(HARDWARE_FIELDS):
field = deepcopy(DeviceForm.base_fields[name])
if name in {"battery", "wifi", "lan", "duplex", "color_printing"}:
field.required = False
field.initial = Device.TriState.UNKNOWN
if self.is_bound and name not in applicable_fields(selected_type):
field.disabled = True
self.fields[name] = field
@property
def hardware_fields(self):
return [self[name] for name in self.fields if name in HARDWARE_FIELDS]
def selected_hardware_values(self):
values = {}
for name in applicable_fields(self.cleaned_data["device_type"]):
value = self.cleaned_data[name]
if name in {"battery", "wifi", "lan", "duplex", "color_printing"} and not value:
value = Device.TriState.UNKNOWN
values[name] = value
return values
def clean_number(self):
number = self.cleaned_data["number"]
if Device.objects.filter(number=number).exists():
raise forms.ValidationError(f"Die Gerätenummer {number} ist bereits vergeben.")
return number
def clean(self):
cleaned = super().clean()
first_number = cleaned.get("number")
quantity = cleaned.get("quantity")
if first_number is None or quantity is None:
return cleaned
last_number = first_number + quantity - 1
if last_number > 2147483647:
self.add_error("quantity", "Die letzte Gerätenummer wäre zu groß.")
return cleaned
conflict = Device.objects.filter(number__range=(first_number, last_number)).order_by("number").values_list("number", flat=True).first()
if conflict is not None:
self.add_error("number", f"Die Gerätenummer {conflict} ist bereits vergeben.")
return cleaned
class DeviceForm(forms.ModelForm):
battery = forms.ChoiceField(label="Akku", choices=Device.TriState.choices, required=False)
wifi = forms.ChoiceField(label="WLAN", choices=Device.TriState.choices, required=False)
lan = forms.ChoiceField(label="LAN", choices=Device.TriState.choices, required=False)
display_inches = forms.DecimalField(label="Zoll", max_digits=4, decimal_places=1, min_value=0, required=False, widget=forms.NumberInput(attrs={"step": "0.1"}))
ram_gb = forms.IntegerField(label="RAM (GB)", min_value=0, required=False)
ssd_gb = forms.IntegerField(label="SSD (GB)", min_value=0, required=False)
hdd_gb = forms.IntegerField(label="HDD (GB)", min_value=0, required=False)
cpu = forms.CharField(label="Prozessor", max_length=200, required=False)
hwid = forms.CharField(label="HWID", max_length=255, required=False)
ports = forms.CharField(label="Anschlüsse", max_length=255, required=False)
resolution = forms.CharField(label="Auflösung", max_length=80, required=False)
print_technology = forms.CharField(label="Druckverfahren", max_length=80, required=False)
duplex = forms.ChoiceField(label="Duplex", choices=Device.TriState.choices, required=False)
color_printing = forms.ChoiceField(label="Farbdruck", choices=Device.TriState.choices, required=False)
class Meta:
model = Device
exclude = ["number", "archived_at", "archived_by", "custom_values", "stage"]
labels = {
"shelf": "Regal", "status": "Bestandsstatus", "date": "Datum", "recipient": "Empfänger",
"location": "Ort", "ticket": "Ticket", "device_type": "Typ", "price": "Preis",
"battery": "Akku", "wifi": "WLAN", "lan": "LAN", "display_inches": "Zoll",
"ram_gb": "RAM (GB)", "ssd_gb": "SSD (GB)", "hdd_gb": "HDD (GB)",
"cpu": "Prozessor", "hwid": "HWID", "model": "Modell", "employee": "Bearbeiter",
"donor": "Spender", "comment": "Kommentar",
}
widgets = {
"date": forms.DateInput(format="%d.%m.%Y", attrs=GERMAN_DATE_WIDGET),
"comment": forms.Textarea(attrs={"rows": 4}),
"price": forms.NumberInput(attrs={"step": "0.01"}),
"display_inches": forms.NumberInput(attrs={"step": "0.1"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.is_bound:
for name in HARDWARE_FIELDS:
self.initial.setdefault(name, getattr(self.instance, name))
selected_type = self.instance.device_type if self.instance.device_type_id else None
if self.is_bound and str(self.data.get("device_type", "")).isdecimal():
selected_type = ChoiceValue.objects.filter(
pk=self.data.get("device_type"), kind=ChoiceValue.Kind.DEVICE_TYPE,
).first()
if self.is_bound:
for name in HARDWARE_FIELDS - applicable_fields(selected_type):
self.fields[name].disabled = True
self.fields["date"].input_formats = GERMAN_DATE_INPUT_FORMATS
mapping = {
"status": ChoiceValue.Kind.STATUS,
"recipient": ChoiceValue.Kind.RECIPIENT,
"location": ChoiceValue.Kind.LOCATION,
"device_type": ChoiceValue.Kind.DEVICE_TYPE,
}
for field_name, kind in mapping.items():
current = getattr(self.instance, f"{field_name}_id", None)
queryset = ChoiceValue.objects.filter(kind=kind, active=True)
if current:
queryset = ChoiceValue.objects.filter(kind=kind).filter(models.Q(active=True) | models.Q(pk=current))
self.fields[field_name].queryset = queryset
current_employee = getattr(self.instance, "employee_id", None)
employee_queryset = Employee.objects.filter(active=True)
if current_employee:
employee_queryset = Employee.objects.filter(models.Q(active=True) | models.Q(pk=current_employee))
self.fields["employee"].queryset = employee_queryset
for field_name in ["ram_gb", "ssd_gb", "hdd_gb", "display_inches"]:
if field_name in self.fields:
self.fields[field_name].widget.attrs["list"] = f"suggestions-{field_name}"
if "cpu" in self.fields:
self.fields["cpu"].widget.attrs.update({"data-cpu-autocomplete": "", "autocomplete": "off"})
def save(self, commit=True):
device = super().save(commit=False)
for name in applicable_fields(device.device_type):
value = self.cleaned_data[name]
if name in {"battery", "wifi", "lan", "duplex", "color_printing"} and not value:
value = Device.TriState.UNKNOWN
setattr(device, name, value)
if commit:
device.save()
self.save_m2m()
return device
class InspectionForm(forms.Form):
defects = forms.CharField(label="Festgestellte Mängel", widget=forms.Textarea(attrs={"rows": 2}), max_length=4000)
model = forms.CharField(label="Modell", max_length=200, required=False)
def __init__(self, *args, device, **kwargs):
super().__init__(*args, **kwargs)
self.device = device
self.initial.setdefault("model", device.model)
for name in ("cpu", "ram_gb", "ssd_gb", "hdd_gb", "display_inches", "battery", "wifi", "lan", "hwid", "ports", "resolution", "print_technology", "duplex", "color_printing"):
if name in applicable_fields(device.device_type):
self.fields[name] = deepcopy(DeviceForm.base_fields[name])
self.initial.setdefault(name, getattr(device, name))
@property
def specification_fields(self):
return [self[name] for name in self.fields if name != "defects"]
class RepairForm(forms.Form):
work_done = forms.CharField(label="Durchgeführte Arbeiten", max_length=4000, widget=forms.Textarea(attrs={"rows": 2}))
class StorageRecipientForm(forms.Form):
recipient = forms.CharField(label="Empfänger", max_length=120)
class EmployeeForm(forms.ModelForm):
class Meta:
model = Employee
fields = ["code", "name", "active"]
labels = {"code": "Kürzel", "name": "Name", "active": "Aktiv"}
help_texts = {"code": "Kurzes eindeutiges Kürzel, zum Beispiel JH."}
class ChoiceValueForm(forms.ModelForm):
color = forms.ChoiceField(
label="Farbe", choices=STATUS_COLOR_CHOICES, required=False,
widget=forms.RadioSelect, help_text="Wird für Statuswerte in Liste und Übersicht verwendet.",
)
class Meta:
model = ChoiceValue
fields = ["kind", "label", "color", "numeric_value", "active", "sort_order"]
labels = {
"kind": "Kategorie", "label": "Bezeichnung", "numeric_value": "Zahlenwert",
"active": "Aktiv", "sort_order": "Sortierreihenfolge",
}
def clean(self):
cleaned = super().clean()
if cleaned.get("kind") == ChoiceValue.Kind.STATUS and not cleaned.get("color"):
cleaned["color"] = STATUS_COLOR_CHOICES[0][0]
return cleaned
class FieldDefinitionForm(forms.ModelForm):
options_text = forms.CharField(
required=False, label="Optionen", help_text="Eine Option pro Zeile (nur für Auswahlfelder).",
widget=forms.Textarea(attrs={"rows": 5}),
)
class Meta:
model = FieldDefinition
fields = [
"name", "key", "field_type", "unit", "required", "active", "show_in_list",
"filterable", "searchable", "sort_order", "group", "allow_custom_values",
]
labels = {
"name": "Name", "key": "Interner Schlüssel", "field_type": "Feldtyp", "unit": "Einheit",
"required": "Pflichtfeld", "active": "Aktiv", "show_in_list": "In der Geräteliste anzeigen",
"filterable": "Filterbar", "searchable": "Durchsuchbar", "sort_order": "Sortierreihenfolge",
"group": "Gruppe", "allow_custom_values": "Eigene Werte erlauben",
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
self.fields["options_text"].initial = "\n".join(self.instance.options.values_list("label", flat=True))
self.fields["key"].disabled = True
self.fields["key"].help_text = "Der interne Key bleibt nach dem Anlegen stabil."
def save(self, commit=True):
instance = super().save(commit=commit)
if commit:
labels = [line.strip() for line in self.cleaned_data.get("options_text", "").splitlines() if line.strip()]
for order, label in enumerate(labels):
FieldOption.objects.update_or_create(field=instance, label=label, defaults={"sort_order": order, "active": True})
instance.options.exclude(label__in=labels).update(active=False)
return instance
class ExcelUploadForm(forms.Form):
file = forms.FileField(label="Excel-Datei (.xlsx)")
def clean_file(self):
uploaded = self.cleaned_data["file"]
if not uploaded.name.lower().endswith(".xlsx"):
raise forms.ValidationError("Bitte eine .xlsx-Datei auswählen.")
if uploaded.size > 10 * 1024 * 1024:
raise forms.ValidationError("Die Datei darf höchstens 10 MB groß sein.")
return uploaded
class UserManagementForm(forms.ModelForm):
role = forms.ChoiceField(label="Rolle", choices=UserProfile.Role.choices)
password = forms.CharField(label="Passwort", required=False, widget=forms.PasswordInput, help_text="Bei bestehenden Konten leer lassen, um das Passwort nicht zu ändern.")
class Meta:
model = User
fields = ["username", "first_name", "last_name", "email", "is_active"]
labels = {
"username": "Benutzername", "first_name": "Vorname", "last_name": "Nachname",
"email": "E-Mail-Adresse", "is_active": "Aktiv",
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
profile, _ = UserProfile.objects.get_or_create(user=self.instance)
self.fields["role"].initial = profile.role
def clean_password(self):
password = self.cleaned_data.get("password", "")
if not self.instance.pk and not password:
raise forms.ValidationError("Für ein neues Konto ist ein Passwort erforderlich.")
if password:
validate_password(password, self.instance if self.instance.pk else None)
return password
def save(self, commit=True):
user = super().save(commit=False)
password = self.cleaned_data.get("password")
if password:
user.set_password(password)
if commit:
user.save()
profile, _ = UserProfile.objects.get_or_create(user=user)
profile.role = UserProfile.Role.ADMIN if user.is_superuser else self.cleaned_data["role"]
profile.save()
return user