Initialize clean CSR inventory repository
This commit is contained in:
564
inventory/models.py
Normal file
564
inventory/models.py
Normal file
@@ -0,0 +1,564 @@
|
||||
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 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 = {
|
||||
"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")
|
||||
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")
|
||||
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)
|
||||
|
||||
|
||||
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")
|
||||
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)
|
||||
|
||||
|
||||
class MonitorSpecification(models.Model):
|
||||
hardware_field_names = ("display_inches", "resolution", "ports")
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user