Files
Inventory/inventory/models.py

596 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)