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