Initialize clean CSR inventory repository

This commit is contained in:
DenisToxic
2026-09-29 08:43:05 +02:00
commit 059f386c60
107 changed files with 9639 additions and 0 deletions

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,46 @@
from datetime import timedelta
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from inventory.models import InventoryAPIKey, UserProfile
from inventory.permissions import can_edit
class Command(BaseCommand):
help = "Erstellt einen zeitlich begrenzten API-Schlüssel und zeigt ihn genau einmal an."
def add_arguments(self, parser):
parser.add_argument("name", help="Eindeutiger Zweck, z. B. inventur-laptop-team2")
parser.add_argument("username", help="Aktives Django-Konto für die Audit-Zuordnung")
parser.add_argument("--write", action="store_true", help="Schreibzugriff erlauben")
parser.add_argument("--read-only", action="store_true", help="Nur Lesezugriff (Standard)")
parser.add_argument("--expires-days", type=int, default=90, help="Gültigkeit in Tagen (Standard: 90)")
def handle(self, *args, **options):
if options["write"] and options["read_only"]:
raise CommandError("--write und --read-only können nicht gemeinsam verwendet werden.")
if options["expires_days"] < 1 or options["expires_days"] > 365:
raise CommandError("Die Gültigkeit muss zwischen 1 und 365 Tagen liegen.")
user_model = get_user_model()
try:
user = user_model.objects.get(username=options["username"], is_active=True)
except user_model.DoesNotExist as exc:
raise CommandError("Das angegebene aktive Konto wurde nicht gefunden.") from exc
if options["write"] and not can_edit(user):
raise CommandError(
f"Schreibzugriff erfordert die Rolle {UserProfile.Role.EDITOR} oder {UserProfile.Role.ADMIN}."
)
api_key, secret = InventoryAPIKey.objects.create_key(
name=options["name"],
service_user=user,
can_read=True,
can_write=options["write"],
expiry_date=timezone.now() + timedelta(days=options["expires_days"]),
)
self.stdout.write(self.style.SUCCESS(f"API-Schlüssel '{api_key.name}' wurde erstellt."))
self.stdout.write(f"Präfix: {api_key.prefix}")
self.stdout.write(f"Läuft ab: {api_key.expiry_date.isoformat()}")
self.stdout.write(self.style.WARNING("Nur jetzt sichtbar – sicher speichern und niemals in Git einchecken:"))
self.stdout.write(secret)

View File

@@ -0,0 +1,53 @@
import random
from datetime import timedelta
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.db.models import Max
from django.utils import timezone
from inventory.models import ChoiceValue, Device, Employee, NumberSequence
from inventory.device_types import bulk_create_specifications, clear_inapplicable
MODELS = ["Lenovo ThinkPad T480", "Dell Latitude 7490", "HP EliteBook 840 G5", "Lenovo ThinkCentre M720", "Dell OptiPlex 7060", "HP ProDesk 600 G4", "Lenovo ThinkPad P50"]
CPUS = ["Intel Core i5-8250U", "Intel Core i5-8350U", "Intel Core i7-8650U", "Intel Core i5-6500", "Intel Core i7-6700", "AMD Ryzen 5 3500U"]
class Command(BaseCommand):
help = "Erzeugt realistische Testgeräte, z. B. generate_test_devices 5000."
def add_arguments(self, parser):
parser.add_argument("count", type=int)
@transaction.atomic
def handle(self, *args, **options):
count = options["count"]
if count < 1 or count > 100000:
raise CommandError("Anzahl muss zwischen 1 und 100000 liegen.")
statuses = list(ChoiceValue.objects.filter(kind=ChoiceValue.Kind.STATUS, active=True))
types = list(ChoiceValue.objects.filter(kind=ChoiceValue.Kind.DEVICE_TYPE, active=True))
employees = list(Employee.objects.filter(active=True))
if not statuses:
raise CommandError("Bitte zuerst seed_initial_data ausführen.")
start = (Device.objects.aggregate(value=Max("number"))["value"] or 0) + 1
today = timezone.localdate()
devices = []
for offset in range(count):
model = random.choice(MODELS)
device = Device(
number=start + offset, shelf=f"{random.choice('ABCDEFGH')}{random.randint(1, 30)}",
status=random.choice(statuses), device_type=random.choice(types) if types else None,
date=today - timedelta(days=random.randint(0, 1500)), model=model, cpu=random.choice(CPUS),
ram_gb=random.choice([4, 8, 8, 16, 32]), ssd_gb=random.choice([0, 128, 256, 512, 1000]),
hdd_gb=random.choice([0, 0, 500, 1000, 2000]), employee=random.choice(employees) if employees else None,
wifi=random.choice(Device.TriState.values), lan=random.choice(Device.TriState.values),
)
clear_inapplicable(device)
device.custom_values = {}
devices.append(device)
for index in range(0, count, 1000):
saved = Device.objects.bulk_create(devices[index:index + 1000], batch_size=1000)
bulk_create_specifications(saved)
NumberSequence.ensure_at_least(start + count - 1)
self.stdout.write(self.style.SUCCESS(f"{count} Testgeräte angelegt."))

View File

@@ -0,0 +1,51 @@
from pathlib import Path
from django.apps import apps
from django.core.management import BaseCommand, CommandError, call_command
from django.core.serializers.base import DeserializationError
from django.db import DatabaseError, connection, transaction
from inventory.models import ChoiceValue, Device, NumberSequence
from django.db.models import Max
class Command(BaseCommand):
help = "Import a SQLite export into an empty, migrated PostgreSQL database."
def add_arguments(self, parser):
parser.add_argument("fixture", type=Path)
def handle(self, *args, **options):
if connection.vendor != "postgresql":
raise CommandError("The destination must be PostgreSQL.")
fixture = options["fixture"].resolve()
if not fixture.is_file():
raise CommandError("Export file does not exist.")
count = self.load_export(fixture, verbosity=options["verbosity"])
self.stdout.write(self.style.SUCCESS(f"Imported {count} devices. SQLite source retained."))
@transaction.atomic
def load_export(self, fixture, *, verbosity=1):
# Migration-created content types and permissions are expected. Business
# data, users and groups must be empty: never merge or overwrite records.
models = [*apps.get_app_config("inventory").get_models(),
apps.get_model("auth", "User"), apps.get_model("auth", "Group")]
for model in models:
if model is ChoiceValue:
# Migration 0003 inserts these two rows even into a fresh DB.
baseline = {("device_type", "Bildschirm", True, 3, "", None),
("device_type", "Drucker", True, 4, "", None)}
rows = set(model.objects.values_list("kind", "label", "active", "sort_order", "color", "numeric_value"))
if rows <= baseline:
continue
if model.objects.exists():
raise CommandError(f"Destination is not empty ({model._meta.label}). No data imported.")
# Only the verified migration defaults can reach this point. Remove them
# within the import transaction so fixture IDs and labels cannot collide.
ChoiceValue.objects.all().delete()
try:
call_command("loaddata", str(fixture), verbosity=verbosity)
except (DeserializationError, DatabaseError) as exc:
raise CommandError("Invalid export or database constraint violation. No data imported.") from exc
NumberSequence.ensure_at_least(Device.objects.aggregate(value=Max("number"))["value"] or 0)
return Device.objects.count()

View File

@@ -0,0 +1,55 @@
from decimal import Decimal
from django.core.management.base import BaseCommand
from django.db import transaction
from inventory.models import ChoiceValue, Employee, NumberSequence
CHOICES = {
ChoiceValue.Kind.STATUS: ["Eingang", "Entsorgt", "Fertig", "Defekt", "Reserviert", "Ausgegeben", "Intern"],
ChoiceValue.Kind.RECIPIENT: ["Landkreis", "Jugendamt", "Bürgergeld", "Rentner", "Geflüchtete", "NGO", "Stadt", "Sonstige", "Verein", "Schüler/Studenten"],
ChoiceValue.Kind.LOCATION: ["R", "L"],
ChoiceValue.Kind.DEVICE_TYPE: ["Laptop", "AiO", "Desktop", "Bildschirm", "Drucker"],
}
NUMERIC_CHOICES = {
ChoiceValue.Kind.RAM: [2, 4, 6, 8, 12, 16, 24, 32, 64, 128],
ChoiceValue.Kind.SSD: [0, 64, 120, 128, 240, 250, 256, 480, 500, 512, 960, 1000, 2000, 4000],
ChoiceValue.Kind.HDD: [0, 64, 120, 128, 240, 250, 256, 480, 500, 512, 960, 1000, 2000, 4000],
ChoiceValue.Kind.DISPLAY: [10.1, 11.6, 12.5, 13.3, 14, 15, 15.6, 16, 17, 17.3],
}
EMPLOYEES = {
"AC": "Ahmad Chaja", "A_H": "Ahmad Hakim", "HA": "Aaron Holmer", "JH": "Johannes Hundshammer",
"TS": "Thomas Stemberger", "YN": "Yurii Naumenko", "VL": "Viktor Lysenko", "SB": "Stefan Bauer",
"KA": "Kanani", "RS": "Ruth Stepper", "SK": "Name aktuell unbekannt", "RG": "Rainer Gerhardt",
"HN": "Heinrich", "KK": "Karsten Kopplin", "LL": "Lucas Lenninger", "AL": "Arseni Litvin", "GW": "Gregor Witczak",
}
STATUS_COLORS = {
"Eingang": "#2563eb", "Entsorgt": "#64748b", "Fertig": "#15803d",
"Defekt": "#dc2626", "Reserviert": "#b45309", "Ausgegeben": "#7c3aed", "Intern": "#0891b2",
}
class Command(BaseCommand):
help = "Legt die initialen Auswahlwerte und Mitarbeiter idempotent an."
@transaction.atomic
def handle(self, *args, **options):
count = 0
for kind, labels in CHOICES.items():
for order, label in enumerate(labels):
defaults = {"active": True, "sort_order": order}
if kind == ChoiceValue.Kind.STATUS:
defaults["color"] = STATUS_COLORS.get(label, "#2563eb")
_, created = ChoiceValue.objects.get_or_create(kind=kind, label=label, defaults=defaults)
count += int(created)
for kind, values in NUMERIC_CHOICES.items():
for order, value in enumerate(values):
label = f"{value:g}"
_, created = ChoiceValue.objects.get_or_create(kind=kind, label=label, defaults={"numeric_value": Decimal(str(value)), "active": True, "sort_order": order})
count += int(created)
for code, name in EMPLOYEES.items():
_, created = Employee.objects.get_or_create(code=code, defaults={"name": name, "active": True})
count += int(created)
NumberSequence.objects.get_or_create(name="device")
self.stdout.write(self.style.SUCCESS(f"Seed-Daten bereit ({count} neue Datensätze)."))