from __future__ import annotations from datetime import timedelta from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.db import models, transaction from django.db.models import Max from django.utils import timezone from rest_framework_api_key.models import AbstractAPIKey, BaseAPIKeyManager class TimeStampedModel(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: abstract = True class UserProfile(TimeStampedModel): class Role(models.TextChoices): ADMIN = "ADMIN", "Administrator" EDITOR = "EDITOR", "Bearbeiter" VIEWER = "VIEWER", "Betrachter" user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") role = models.CharField(max_length=10, choices=Role.choices, default=Role.VIEWER) def __str__(self) -> str: return f"{self.user.username} ({self.get_role_display()})" class ChoiceValue(TimeStampedModel): class Kind(models.TextChoices): STATUS = "status", "Status" RECIPIENT = "recipient", "Empfänger" LOCATION = "location", "Ort" DEVICE_TYPE = "device_type", "Typ" RAM = "ram", "RAM-Vorschlag" SSD = "ssd", "SSD-Vorschlag" HDD = "hdd", "HDD-Vorschlag" DISPLAY = "display", "Displaygröße" kind = models.CharField(max_length=30, choices=Kind.choices, db_index=True) label = models.CharField(max_length=120) color = models.CharField(max_length=20, blank=True) numeric_value = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) active = models.BooleanField(default=True, db_index=True) sort_order = models.PositiveIntegerField(default=0) class Meta: ordering = ["kind", "sort_order", "label"] constraints = [models.UniqueConstraint(fields=["kind", "label"], name="unique_choice_label_per_kind")] indexes = [models.Index(fields=["kind", "active", "sort_order"])] def __str__(self) -> str: return self.label class Employee(TimeStampedModel): code = models.CharField(max_length=12, unique=True) name = models.CharField(max_length=160) active = models.BooleanField(default=True, db_index=True) class Meta: ordering = ["code"] def __str__(self) -> str: return f"{self.code} – {self.name}" class FieldDefinition(TimeStampedModel): class FieldType(models.TextChoices): TEXT = "text", "Text" LONG_TEXT = "long_text", "Langer Text" INTEGER = "integer", "Ganzzahl" DECIMAL = "decimal", "Dezimalzahl" BOOLEAN = "boolean", "Ja/Nein" DATE = "date", "Datum" SELECT = "select", "Auswahl" MULTI_SELECT = "multi_select", "Mehrfachauswahl" name = models.CharField(max_length=120) key = models.SlugField(max_length=80, unique=True) field_type = models.CharField(max_length=20, choices=FieldType.choices) unit = models.CharField(max_length=30, blank=True) required = models.BooleanField(default=False) active = models.BooleanField(default=True, db_index=True) show_in_list = models.BooleanField(default=False) filterable = models.BooleanField(default=False) searchable = models.BooleanField(default=False) sort_order = models.PositiveIntegerField(default=0) group = models.CharField(max_length=80, default="Zusatzfelder") allow_custom_values = models.BooleanField(default=False) class Meta: ordering = ["sort_order", "name"] def __str__(self) -> str: return self.name class DeviceRegistration(TimeStampedModel): """Durable receipt so retrying a USB report cannot allocate another device.""" request_id = models.UUIDField(unique=True) payload_hash = models.CharField(max_length=64) device = models.OneToOneField("Device", on_delete=models.PROTECT, related_name="registration") class FieldOption(TimeStampedModel): field = models.ForeignKey(FieldDefinition, on_delete=models.CASCADE, related_name="options") label = models.CharField(max_length=120) active = models.BooleanField(default=True) sort_order = models.PositiveIntegerField(default=0) class Meta: ordering = ["sort_order", "label"] constraints = [models.UniqueConstraint(fields=["field", "label"], name="unique_custom_field_option")] def __str__(self) -> str: return self.label class NumberSequence(models.Model): name = models.CharField(max_length=30, primary_key=True) last_value = models.PositiveIntegerField(default=0) @classmethod def next_device_number(cls) -> int: with transaction.atomic(): sequence, _ = cls.objects.select_for_update().get_or_create(name="device") maximum = Device.objects.aggregate(value=Max("number"))["value"] or 0 sequence.last_value = max(sequence.last_value, maximum) + 1 sequence.save(update_fields=["last_value"]) return sequence.last_value @classmethod def suggested_device_number(cls) -> int: sequence_value = cls.objects.filter(name="device").values_list("last_value", flat=True).first() or 0 maximum = Device.objects.aggregate(value=Max("number"))["value"] or 0 return max(sequence_value, maximum) + 1 @classmethod def ensure_at_least(cls, value: int) -> None: with transaction.atomic(): sequence, _ = cls.objects.select_for_update().get_or_create(name="device") if sequence.last_value < value: sequence.last_value = value sequence.save(update_fields=["last_value"]) HARDWARE_DEFAULTS = { "camera": "UNKNOWN", "sound": "UNKNOWN", "hdmi": "UNKNOWN", "vga": "UNKNOWN", "dvi": "UNKNOWN", "displayport": "UNKNOWN", "aspect_ratio": "", "battery": "UNKNOWN", "wifi": "UNKNOWN", "lan": "UNKNOWN", "display_inches": None, "ram_gb": None, "ssd_gb": None, "hdd_gb": None, "cpu": "", "hwid": "", "ports": "", "resolution": "", "print_technology": "", "duplex": "UNKNOWN", "color_printing": "UNKNOWN", } class HardwareValue: """Compatibility attribute backed by one type-specific specification row.""" def __init__(self, name): self.name = name def __get__(self, instance, owner): if instance is None: return self instance._load_hardware() return instance._hardware_cache[self.name] def __set__(self, instance, value): instance._load_hardware() instance._hardware_cache[self.name] = value instance._hardware_dirty = True class Device(TimeStampedModel): # A revision is independent of the resolution of the operating system clock. revision = models.PositiveBigIntegerField(default=1, editable=False) updated_at = models.DateTimeField(default=timezone.now, editable=False) class TriState(models.TextChoices): YES = "YES", "JA" NO = "NO", "NEIN" UNKNOWN = "UNKNOWN", "UNBEKANNT" class Stage(models.TextChoices): INTAKE = "INTAKE", "Eingang" INSPECTION = "INSPECTION", "Inspektion" REPAIR = "REPAIR", "Reparatur" STORAGE = "STORAGE", "Lagerung" ISSUED = "ISSUED", "Ausgegeben" stage = models.CharField(max_length=12, choices=Stage.choices, default=Stage.INTAKE, db_index=True) number = models.PositiveIntegerField(unique=True, db_index=True, editable=False) shelf = models.CharField(max_length=80, blank=True, db_index=True) status = models.ForeignKey(ChoiceValue, on_delete=models.PROTECT, related_name="status_devices", limit_choices_to={"kind": ChoiceValue.Kind.STATUS}) date = models.DateField(default=timezone.localdate, db_index=True) recipient = models.ForeignKey(ChoiceValue, on_delete=models.PROTECT, null=True, blank=True, related_name="recipient_devices", limit_choices_to={"kind": ChoiceValue.Kind.RECIPIENT}) location = models.ForeignKey(ChoiceValue, on_delete=models.PROTECT, null=True, blank=True, related_name="location_devices", limit_choices_to={"kind": ChoiceValue.Kind.LOCATION}) ticket = models.CharField(max_length=120, blank=True, db_index=True) device_type = models.ForeignKey(ChoiceValue, on_delete=models.PROTECT, null=True, blank=True, related_name="type_devices", limit_choices_to={"kind": ChoiceValue.Kind.DEVICE_TYPE}) price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) battery = HardwareValue("battery") wifi = HardwareValue("wifi") lan = HardwareValue("lan") display_inches = HardwareValue("display_inches") ram_gb = HardwareValue("ram_gb") ssd_gb = HardwareValue("ssd_gb") hdd_gb = HardwareValue("hdd_gb") cpu = HardwareValue("cpu") hwid = HardwareValue("hwid") ports = HardwareValue("ports") camera = HardwareValue("camera") sound = HardwareValue("sound") hdmi = HardwareValue("hdmi") vga = HardwareValue("vga") dvi = HardwareValue("dvi") displayport = HardwareValue("displayport") aspect_ratio = HardwareValue("aspect_ratio") resolution = HardwareValue("resolution") print_technology = HardwareValue("print_technology") duplex = HardwareValue("duplex") color_printing = HardwareValue("color_printing") model = models.CharField(max_length=200, blank=True, db_index=True) employee = models.ForeignKey(Employee, on_delete=models.PROTECT, null=True, blank=True, related_name="devices") donor = models.CharField(max_length=200, blank=True) comment = models.TextField(blank=True) custom_values = models.JSONField(default=dict, blank=True) archived_at = models.DateTimeField(null=True, blank=True, db_index=True) archived_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="archived_devices") class Meta: ordering = ["-number"] constraints = [models.CheckConstraint(condition=models.Q(stage__in=["INTAKE", "INSPECTION", "REPAIR", "STORAGE", "ISSUED"]), name="valid_device_stage")] indexes = [ models.Index(fields=["archived_at", "-number"]), models.Index(fields=["status", "archived_at"]), models.Index(fields=["device_type", "archived_at"]), models.Index(fields=["employee", "archived_at"]), models.Index(fields=["recipient", "archived_at"]), ] def __init__(self, *args, **kwargs): hardware = {name: kwargs.pop(name) for name in HARDWARE_DEFAULTS if name in kwargs} super().__init__(*args, **kwargs) self._loaded_device_type_id = self.device_type_id self._hardware_cache = {**HARDWARE_DEFAULTS, **hardware} if hardware else None self._hardware_dirty = bool(hardware) def _load_hardware(self): if self._hardware_cache is not None: return values = dict(HARDWARE_DEFAULTS) if self.pk and not self._state.adding: old_type = self.device_type if self._loaded_device_type_id == self.device_type_id else ( ChoiceValue.objects.filter(pk=self._loaded_device_type_id).first() if self._loaded_device_type_id else None ) model = specification_model(old_type) specification = getattr(self, model._meta.get_field("device").remote_field.related_name, None) if specification: for name in model.hardware_field_names: values[name] = getattr(specification, name) self._hardware_cache = values def _save_hardware(self): model = specification_model(self.device_type) for other in SPECIFICATION_MODELS: if other is not model: other.objects.filter(device_id=self.pk).delete() defaults = {name: self._hardware_cache[name] for name in model.hardware_field_names} specification, _ = model.objects.update_or_create(device=self, defaults=defaults) specification.full_clean() self._hardware_dirty = False self._loaded_device_type_id = self.device_type_id @transaction.atomic def save(self, *args, **kwargs) -> None: creating = self._state.adding type_changed = not creating and self.device_type_id != self._loaded_device_type_id if creating or type_changed or self._hardware_dirty: self._load_hardware() if creating or type_changed: from .device_types import clear_inapplicable cleared = clear_inapplicable(self) if cleared and kwargs.get("update_fields") is not None: kwargs["update_fields"] = set(kwargs["update_fields"]) | {"custom_values"} if kwargs.get("update_fields") is not None: kwargs["update_fields"] = set(kwargs["update_fields"]) - HARDWARE_DEFAULTS.keys() if self._hardware_dirty and not kwargs["update_fields"]: kwargs["update_fields"].add("updated_at") if not creating: previous = Device.objects.select_for_update().get(pk=self.pk) self.revision = previous.revision + 1 # Keep legacy API timestamp tokens unique, including on coarse clocks. self.updated_at = max(timezone.now(), previous.updated_at + timedelta(microseconds=1)) if kwargs.get("update_fields") is not None: if not kwargs["update_fields"]: return kwargs["update_fields"] = set(kwargs["update_fields"]) | {"revision", "updated_at"} if not self.number: self.number = NumberSequence.next_device_number() super().save(*args, **kwargs) if creating: NumberSequence.ensure_at_least(self.number) if creating or type_changed or self._hardware_dirty: self._save_hardware() def refresh_from_db(self, *args, **kwargs): super().refresh_from_db(*args, **kwargs) self._loaded_device_type_id = self.device_type_id self._hardware_cache = None self._hardware_dirty = False def __str__(self) -> str: return f"Gerät #{self.number}" @property def hardware_fields(self): from .device_types import applicable_fields return applicable_fields(self.device_type) def get_battery_display(self): return self.TriState(self.battery).label def get_wifi_display(self): return self.TriState(self.wifi).label def get_lan_display(self): return self.TriState(self.lan).label class ComputerSpecification(models.Model): hardware_field_names = ("cpu", "hwid", "ram_gb", "ssd_gb", "hdd_gb", "display_inches", "battery", "wifi", "lan", "ports") device = models.OneToOneField(Device, on_delete=models.CASCADE, related_name="computer_specification", primary_key=True) cpu = models.CharField(max_length=200, blank=True, db_index=True) hwid = models.CharField(max_length=255, blank=True, db_index=True) ram_gb = models.PositiveIntegerField(null=True, blank=True, db_index=True) ssd_gb = models.PositiveIntegerField(null=True, blank=True, db_index=True) hdd_gb = models.PositiveIntegerField(null=True, blank=True, db_index=True) display_inches = models.DecimalField(max_digits=4, decimal_places=1, null=True, blank=True) battery = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) wifi = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) lan = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) ports = models.CharField(max_length=255, blank=True) class ComputerCore(models.Model): cpu = models.CharField(max_length=200, blank=True, db_index=True) hwid = models.CharField(max_length=255, blank=True, db_index=True) ram_gb = models.PositiveIntegerField(null=True, blank=True, db_index=True) ssd_gb = models.PositiveIntegerField(null=True, blank=True, db_index=True) hdd_gb = models.PositiveIntegerField(null=True, blank=True, db_index=True) wifi = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) lan = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) ports = models.CharField(max_length=255, blank=True) class Meta: abstract = True class LaptopSpecification(ComputerCore): hardware_field_names = ("cpu", "hwid", "ram_gb", "ssd_gb", "hdd_gb", "display_inches", "battery", "wifi", "lan", "ports", "camera", "sound") device = models.OneToOneField(Device, on_delete=models.CASCADE, related_name="laptop_specification", primary_key=True) display_inches = models.DecimalField(max_digits=4, decimal_places=1, null=True, blank=True) battery = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) camera = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) sound = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) class DesktopSpecification(ComputerCore): hardware_field_names = ("cpu", "hwid", "ram_gb", "ssd_gb", "hdd_gb", "wifi", "lan", "ports") device = models.OneToOneField(Device, on_delete=models.CASCADE, related_name="desktop_specification", primary_key=True) class AllInOneSpecification(ComputerCore): hardware_field_names = ("cpu", "hwid", "ram_gb", "ssd_gb", "hdd_gb", "display_inches", "wifi", "lan", "ports", "camera", "sound") device = models.OneToOneField(Device, on_delete=models.CASCADE, related_name="aio_specification", primary_key=True) display_inches = models.DecimalField(max_digits=4, decimal_places=1, null=True, blank=True) camera = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) sound = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) class MonitorSpecification(models.Model): hardware_field_names = ("display_inches", "resolution", "ports", "hdmi", "vga", "dvi", "displayport", "aspect_ratio") device = models.OneToOneField(Device, on_delete=models.CASCADE, related_name="monitor_specification", primary_key=True) display_inches = models.DecimalField(max_digits=4, decimal_places=1, null=True, blank=True) resolution = models.CharField(max_length=80, blank=True) ports = models.CharField(max_length=255, blank=True) hdmi = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) vga = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) dvi = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) displayport = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) aspect_ratio = models.CharField(max_length=20, blank=True) class PrinterSpecification(models.Model): hardware_field_names = ("wifi", "lan", "ports", "print_technology", "duplex", "color_printing") device = models.OneToOneField(Device, on_delete=models.CASCADE, related_name="printer_specification", primary_key=True) wifi = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) lan = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) ports = models.CharField(max_length=255, blank=True) print_technology = models.CharField(max_length=80, blank=True) duplex = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) color_printing = models.CharField(max_length=10, choices=Device.TriState.choices, default=Device.TriState.UNKNOWN) SPECIFICATION_MODELS = (LaptopSpecification, DesktopSpecification, AllInOneSpecification, MonitorSpecification, PrinterSpecification, ComputerSpecification) def specification_model(device_type): label = device_type.label.strip().casefold() if device_type else "" if label in {"drucker", "printer"}: return PrinterSpecification if label in {"bildschirm", "monitor"}: return MonitorSpecification if label == "laptop": return LaptopSpecification if label == "desktop": return DesktopSpecification if label in {"aio", "all-in-one", "all in one"}: return AllInOneSpecification return ComputerSpecification class DeviceTask(TimeStampedModel): class State(models.TextChoices): OPEN = "OPEN", "Offen" DONE = "DONE", "Erledigt" device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name="tasks") title = models.CharField(max_length=200) description = models.TextField(blank=True) state = models.CharField(max_length=8, choices=State.choices, default=State.OPEN, db_index=True) assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_device_tasks") created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="created_device_tasks") completed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="completed_device_tasks") completed_at = models.DateTimeField(null=True, blank=True) due_date = models.DateField(null=True, blank=True) class Meta: ordering = ["state", "-created_at"] constraints = [ models.CheckConstraint(condition=models.Q(state__in=["OPEN", "DONE"]), name="valid_device_task_state"), models.CheckConstraint( condition=models.Q(state="OPEN", completed_at__isnull=True) | models.Q(state="DONE", completed_at__isnull=False), name="device_task_completion_matches_state", ), ] indexes = [models.Index(fields=["state", "due_date"]), models.Index(fields=["device", "state"])] class WorkflowEvent(models.Model): device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name="workflow_events") from_stage = models.CharField(max_length=12, choices=Device.Stage.choices, blank=True) to_stage = models.CharField(max_length=12, choices=Device.Stage.choices) note = models.CharField(max_length=500, blank=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["-created_at", "-id"] indexes = [models.Index(fields=["device", "-created_at"])] class InspectionRecord(models.Model): device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name="inspections") defects = models.TextField() recorded_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["-created_at", "-id"] indexes = [models.Index(fields=["device", "-created_at"])] class RepairRecord(models.Model): device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name="repairs") work_done = models.TextField() recorded_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["-created_at", "-id"] indexes = [models.Index(fields=["device", "-created_at"])] class AuditLog(models.Model): class Action(models.TextChoices): CREATE = "CREATE", "Erstellt" UPDATE = "UPDATE", "Geändert" ARCHIVE = "ARCHIVE", "Archiviert" RESTORE = "RESTORE", "Wiederhergestellt" IMPORT = "IMPORT", "Importiert" user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="audit_entries") api_client = models.CharField(max_length=50, blank=True) device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name="audit_entries") action = models.CharField(max_length=10, choices=Action.choices) field = models.CharField(max_length=100, blank=True) field_key = models.CharField(max_length=120, blank=True) old_value = models.TextField(blank=True) new_value = models.TextField(blank=True) old_data = models.JSONField(null=True, blank=True) new_data = models.JSONField(null=True, blank=True) reverted_at = models.DateTimeField(null=True, blank=True) reverted_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="reverted_audit_entries", ) created_at = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: ordering = ["-created_at", "-id"] indexes = [models.Index(fields=["device", "-created_at"])] class InventoryAPIKeyManager(BaseAPIKeyManager): def get_usable_keys(self): return super().get_usable_keys().filter( service_user__is_active=True, expiry_date__gt=timezone.now(), ) class InventoryAPIKey(AbstractAPIKey): """Widerrufbarer Schlüssel für genau einen internen API-Client.""" objects = InventoryAPIKeyManager() service_user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="inventory_api_keys", help_text="Technisches oder persönliches Konto, dem API-Änderungen zugerechnet werden.", ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="created_inventory_api_keys", ) can_read = models.BooleanField(default=True) can_write = models.BooleanField(default=False) last_used_at = models.DateTimeField(null=True, blank=True, editable=False) last_used_ip = models.GenericIPAddressField(null=True, blank=True, editable=False) class Meta(AbstractAPIKey.Meta): verbose_name = "API-Schlüssel" verbose_name_plural = "API-Schlüssel" class UserPreference(TimeStampedModel): user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="inventory_preferences") visible_columns = models.JSONField(default=list, blank=True) column_order = models.JSONField(default=list, blank=True) column_widths = models.JSONField(default=dict, blank=True) page_size = models.PositiveSmallIntegerField(default=50) class SavedView(TimeStampedModel): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="saved_views") name = models.CharField(max_length=120) filters = models.JSONField(default=dict) class Meta: constraints = [models.UniqueConstraint(fields=["user", "name"], name="unique_saved_view_name")] class ImportJob(TimeStampedModel): class Status(models.TextChoices): UPLOADED = "UPLOADED", "Hochgeladen" COMPLETED = "COMPLETED", "Abgeschlossen" FAILED = "FAILED", "Fehlgeschlagen" user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="import_jobs") file = models.FileField(upload_to="imports/%Y/%m/") original_name = models.CharField(max_length=255) status = models.CharField(max_length=12, choices=Status.choices, default=Status.UPLOADED) headers = models.JSONField(default=list) preview_rows = models.JSONField(default=list) result = models.JSONField(default=dict)