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,233 @@
# Generated by Django 5.2.17 on 2026-09-19 12:26
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Employee',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('code', models.CharField(max_length=12, unique=True)),
('name', models.CharField(max_length=160)),
('active', models.BooleanField(db_index=True, default=True)),
],
options={
'ordering': ['code'],
},
),
migrations.CreateModel(
name='FieldDefinition',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=120)),
('key', models.SlugField(max_length=80, unique=True)),
('field_type', models.CharField(choices=[('text', 'Text'), ('long_text', 'Langer Text'), ('integer', 'Ganzzahl'), ('decimal', 'Dezimalzahl'), ('boolean', 'Ja/Nein'), ('date', 'Datum'), ('select', 'Auswahl'), ('multi_select', 'Mehrfachauswahl')], max_length=20)),
('unit', models.CharField(blank=True, max_length=30)),
('required', models.BooleanField(default=False)),
('active', models.BooleanField(db_index=True, default=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(default='Zusatzfelder', max_length=80)),
('allow_custom_values', models.BooleanField(default=False)),
],
options={
'ordering': ['sort_order', 'name'],
},
),
migrations.CreateModel(
name='NumberSequence',
fields=[
('name', models.CharField(max_length=30, primary_key=True, serialize=False)),
('last_value', models.PositiveIntegerField(default=0)),
],
),
migrations.CreateModel(
name='ChoiceValue',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('kind', models.CharField(choices=[('status', 'Status'), ('recipient', 'Empfänger'), ('location', 'Ort'), ('device_type', 'Typ'), ('ram', 'RAM-Vorschlag'), ('ssd', 'SSD-Vorschlag'), ('hdd', 'HDD-Vorschlag'), ('display', 'Displaygröße')], db_index=True, max_length=30)),
('label', models.CharField(max_length=120)),
('numeric_value', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('active', models.BooleanField(db_index=True, default=True)),
('sort_order', models.PositiveIntegerField(default=0)),
],
options={
'ordering': ['kind', 'sort_order', 'label'],
'indexes': [models.Index(fields=['kind', 'active', 'sort_order'], name='inventory_c_kind_320c2f_idx')],
'constraints': [models.UniqueConstraint(fields=('kind', 'label'), name='unique_choice_label_per_kind')],
},
),
migrations.CreateModel(
name='Device',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('number', models.PositiveIntegerField(db_index=True, editable=False, unique=True)),
('shelf', models.CharField(blank=True, db_index=True, max_length=80)),
('date', models.DateField(db_index=True, default=django.utils.timezone.localdate)),
('ticket', models.CharField(blank=True, db_index=True, max_length=120)),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('battery', models.CharField(choices=[('YES', 'JA'), ('NO', 'NEIN'), ('UNKNOWN', 'UNBEKANNT')], default='UNKNOWN', max_length=10)),
('wifi', models.CharField(choices=[('YES', 'JA'), ('NO', 'NEIN'), ('UNKNOWN', 'UNBEKANNT')], default='UNKNOWN', max_length=10)),
('lan', models.CharField(choices=[('YES', 'JA'), ('NO', 'NEIN'), ('UNKNOWN', 'UNBEKANNT')], default='UNKNOWN', max_length=10)),
('display_inches', models.DecimalField(blank=True, decimal_places=1, max_digits=4, null=True)),
('ram_gb', models.PositiveIntegerField(blank=True, db_index=True, null=True)),
('ssd_gb', models.PositiveIntegerField(blank=True, db_index=True, null=True)),
('hdd_gb', models.PositiveIntegerField(blank=True, db_index=True, null=True)),
('cpu', models.CharField(blank=True, db_index=True, max_length=200)),
('model', models.CharField(blank=True, db_index=True, max_length=200)),
('donor', models.CharField(blank=True, max_length=200)),
('comment', models.TextField(blank=True)),
('custom_values', models.JSONField(blank=True, default=dict)),
('archived_at', models.DateTimeField(blank=True, db_index=True, null=True)),
('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_devices', to=settings.AUTH_USER_MODEL)),
('device_type', models.ForeignKey(blank=True, limit_choices_to={'kind': 'device_type'}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='type_devices', to='inventory.choicevalue')),
('location', models.ForeignKey(blank=True, limit_choices_to={'kind': 'location'}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='location_devices', to='inventory.choicevalue')),
('recipient', models.ForeignKey(blank=True, limit_choices_to={'kind': 'recipient'}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='recipient_devices', to='inventory.choicevalue')),
('status', models.ForeignKey(limit_choices_to={'kind': 'status'}, on_delete=django.db.models.deletion.PROTECT, related_name='status_devices', to='inventory.choicevalue')),
('employee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='inventory.employee')),
],
options={
'ordering': ['-number'],
},
),
migrations.CreateModel(
name='FieldOption',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('label', models.CharField(max_length=120)),
('active', models.BooleanField(default=True)),
('sort_order', models.PositiveIntegerField(default=0)),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='inventory.fielddefinition')),
],
options={
'ordering': ['sort_order', 'label'],
},
),
migrations.CreateModel(
name='ImportJob',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('file', models.FileField(upload_to='imports/%Y/%m/')),
('original_name', models.CharField(max_length=255)),
('status', models.CharField(choices=[('UPLOADED', 'Hochgeladen'), ('COMPLETED', 'Abgeschlossen'), ('FAILED', 'Fehlgeschlagen')], default='UPLOADED', max_length=12)),
('headers', models.JSONField(default=list)),
('preview_rows', models.JSONField(default=list)),
('result', models.JSONField(default=dict)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='import_jobs', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='SavedView',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=120)),
('filters', models.JSONField(default=dict)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='saved_views', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='UserPreference',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('visible_columns', models.JSONField(blank=True, default=list)),
('column_order', models.JSONField(blank=True, default=list)),
('column_widths', models.JSONField(blank=True, default=dict)),
('page_size', models.PositiveSmallIntegerField(default=50)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_preferences', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('role', models.CharField(choices=[('ADMIN', 'Administrator'), ('EDITOR', 'Bearbeiter'), ('VIEWER', 'Betrachter')], default='VIEWER', max_length=10)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='AuditLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action', models.CharField(choices=[('CREATE', 'Erstellt'), ('UPDATE', 'Geändert'), ('ARCHIVE', 'Archiviert'), ('RESTORE', 'Wiederhergestellt'), ('IMPORT', 'Importiert')], max_length=10)),
('field', models.CharField(blank=True, max_length=100)),
('old_value', models.TextField(blank=True)),
('new_value', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audit_entries', to=settings.AUTH_USER_MODEL)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='audit_entries', to='inventory.device')),
],
options={
'ordering': ['-created_at', '-id'],
'indexes': [models.Index(fields=['device', '-created_at'], name='inventory_a_device__78ae29_idx')],
},
),
migrations.AddIndex(
model_name='device',
index=models.Index(fields=['archived_at', '-number'], name='inventory_d_archive_2483fe_idx'),
),
migrations.AddIndex(
model_name='device',
index=models.Index(fields=['status', 'archived_at'], name='inventory_d_status__f41a2e_idx'),
),
migrations.AddIndex(
model_name='device',
index=models.Index(fields=['device_type', 'archived_at'], name='inventory_d_device__ec71d5_idx'),
),
migrations.AddIndex(
model_name='device',
index=models.Index(fields=['employee', 'archived_at'], name='inventory_d_employe_e23a79_idx'),
),
migrations.AddIndex(
model_name='device',
index=models.Index(fields=['recipient', 'archived_at'], name='inventory_d_recipie_5290ea_idx'),
),
migrations.AddConstraint(
model_name='fieldoption',
constraint=models.UniqueConstraint(fields=('field', 'label'), name='unique_custom_field_option'),
),
migrations.AddConstraint(
model_name='savedview',
constraint=models.UniqueConstraint(fields=('user', 'name'), name='unique_saved_view_name'),
),
]

View File

@@ -0,0 +1,30 @@
from django.db import migrations
def add_postgres_indexes(apps, schema_editor):
if schema_editor.connection.vendor != "postgresql":
return
statements = [
"CREATE EXTENSION IF NOT EXISTS pg_trgm",
"CREATE INDEX IF NOT EXISTS inventory_device_cpu_trgm ON inventory_device USING gin (cpu gin_trgm_ops)",
"CREATE INDEX IF NOT EXISTS inventory_device_model_trgm ON inventory_device USING gin (model gin_trgm_ops)",
"CREATE INDEX IF NOT EXISTS inventory_device_ticket_trgm ON inventory_device USING gin (ticket gin_trgm_ops)",
"CREATE INDEX IF NOT EXISTS inventory_device_donor_trgm ON inventory_device USING gin (donor gin_trgm_ops)",
"CREATE INDEX IF NOT EXISTS inventory_device_custom_values_gin ON inventory_device USING gin (custom_values jsonb_path_ops)",
]
with schema_editor.connection.cursor() as cursor:
for statement in statements:
cursor.execute(statement)
def remove_postgres_indexes(apps, schema_editor):
if schema_editor.connection.vendor != "postgresql":
return
with schema_editor.connection.cursor() as cursor:
for name in ["inventory_device_cpu_trgm", "inventory_device_model_trgm", "inventory_device_ticket_trgm", "inventory_device_donor_trgm", "inventory_device_custom_values_gin"]:
cursor.execute(f"DROP INDEX IF EXISTS {name}")
class Migration(migrations.Migration):
dependencies = [("inventory", "0001_initial")]
operations = [migrations.RunPython(add_postgres_indexes, remove_postgres_indexes)]

View File

@@ -0,0 +1,51 @@
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
def add_device_types(apps, schema_editor):
ChoiceValue = apps.get_model("inventory", "ChoiceValue")
for order, label in enumerate(("Bildschirm", "Drucker"), start=3):
ChoiceValue.objects.update_or_create(
kind="device_type", label=label,
defaults={"active": True, "sort_order": order},
)
def remove_device_types(apps, schema_editor):
ChoiceValue = apps.get_model("inventory", "ChoiceValue")
ChoiceValue.objects.filter(kind="device_type", label__in=("Bildschirm", "Drucker")).delete()
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("inventory", "0002_postgres_search_indexes"),
]
operations = [
migrations.AddField(
model_name="auditlog", name="field_key",
field=models.CharField(blank=True, max_length=120),
),
migrations.AddField(
model_name="auditlog", name="old_data",
field=models.JSONField(blank=True, null=True),
),
migrations.AddField(
model_name="auditlog", name="new_data",
field=models.JSONField(blank=True, null=True),
),
migrations.AddField(
model_name="auditlog", name="reverted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="auditlog", name="reverted_by",
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name="reverted_audit_entries", to=settings.AUTH_USER_MODEL,
),
),
migrations.RunPython(add_device_types, remove_device_types),
]

View File

@@ -0,0 +1,24 @@
from django.db import migrations, models
STATUS_COLORS = {
"Eingang": "#2563eb", "Entsorgt": "#64748b", "Fertig": "#15803d",
"Defekt": "#dc2626", "Reserviert": "#b45309", "Ausgegeben": "#7c3aed", "Intern": "#0891b2",
}
def set_status_colors(apps, schema_editor):
ChoiceValue = apps.get_model("inventory", "ChoiceValue")
for label, color in STATUS_COLORS.items():
ChoiceValue.objects.filter(kind="status", label=label).update(color=color)
class Migration(migrations.Migration):
dependencies = [("inventory", "0003_audit_revert_and_device_types")]
operations = [
migrations.AddField(
model_name="choicevalue", name="color",
field=models.CharField(blank=True, max_length=20),
),
migrations.RunPython(set_status_colors, migrations.RunPython.noop),
]

View File

@@ -0,0 +1,50 @@
# Generated by Django 5.2.17 on 2026-09-21 13:01
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0004_choicevalue_color'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='auditlog',
name='api_client',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='device',
name='hwid',
field=models.CharField(blank=True, db_index=True, max_length=255),
),
migrations.CreateModel(
name='InventoryAPIKey',
fields=[
('id', models.CharField(editable=False, max_length=150, primary_key=True, serialize=False, unique=True)),
('prefix', models.CharField(editable=False, max_length=8, unique=True)),
('hashed_key', models.CharField(editable=False, max_length=150)),
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
('name', models.CharField(default=None, help_text='A free-form name for the API key. Need not be unique. 50 characters max.', max_length=50)),
('revoked', models.BooleanField(blank=True, default=False, help_text='If the API key is revoked, clients cannot use it anymore. (This cannot be undone.)')),
('expiry_date', models.DateTimeField(blank=True, help_text='Once API key expires, clients cannot use it anymore.', null=True, verbose_name='Expires')),
('can_read', models.BooleanField(default=True)),
('can_write', models.BooleanField(default=False)),
('last_used_at', models.DateTimeField(blank=True, editable=False, null=True)),
('last_used_ip', models.GenericIPAddressField(blank=True, editable=False, null=True)),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_inventory_api_keys', to=settings.AUTH_USER_MODEL)),
('service_user', models.ForeignKey(help_text='Technisches oder persönliches Konto, dem API-Änderungen zugerechnet werden.', on_delete=django.db.models.deletion.PROTECT, related_name='inventory_api_keys', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'API-Schlüssel',
'verbose_name_plural': 'API-Schlüssel',
'ordering': ('-created',),
'abstract': False,
},
),
]

View File

@@ -0,0 +1,16 @@
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [("inventory", "0005_auditlog_api_client_device_hwid_inventoryapikey")]
operations = [
migrations.AddField(
model_name="device", name="revision",
field=models.PositiveBigIntegerField(default=1, editable=False),
),
migrations.AlterField(
model_name="device", name="updated_at",
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
]

View File

@@ -0,0 +1,67 @@
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def initial_stages(apps, schema_editor):
Device = apps.get_model("inventory", "Device")
for device in Device.objects.select_related("status").iterator():
label = device.status.label.casefold()
if label == "ausgegeben":
stage = "ISSUED"
elif label in {"fertig", "reserviert"}:
stage = "STORAGE"
elif label == "defekt":
stage = "REPAIR"
else:
stage = "INTAKE"
Device.objects.filter(pk=device.pk).update(stage=stage)
class Migration(migrations.Migration):
dependencies = [
("inventory", "0006_device_revision"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="device", name="stage",
field=models.CharField(max_length=12, choices=[
("INTAKE", "Eingang"), ("INSPECTION", "Inspektion"),
("REPAIR", "Reparatur"), ("STORAGE", "Lagerung"), ("ISSUED", "Ausgegeben"),
], default="INTAKE", db_index=True),
),
migrations.CreateModel(
name="DeviceTask",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("title", models.CharField(max_length=200)),
("description", models.TextField(blank=True)),
("state", models.CharField(max_length=8, choices=[("OPEN", "Offen"), ("DONE", "Erledigt")], default="OPEN", db_index=True)),
("completed_at", models.DateTimeField(null=True, blank=True)),
("due_date", models.DateField(null=True, blank=True)),
("assigned_to", models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=django.db.models.deletion.SET_NULL, related_name="assigned_device_tasks", null=True, blank=True)),
("completed_by", models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=django.db.models.deletion.SET_NULL, related_name="completed_device_tasks", null=True, blank=True)),
("created_by", models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=django.db.models.deletion.SET_NULL, related_name="created_device_tasks", null=True)),
("device", models.ForeignKey(to="inventory.device", on_delete=django.db.models.deletion.CASCADE, related_name="tasks")),
],
options={"ordering": ["state", "-created_at"]},
),
migrations.CreateModel(
name="WorkflowEvent",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("from_stage", models.CharField(max_length=12, choices=[("INTAKE", "Eingang"), ("INSPECTION", "Inspektion"), ("REPAIR", "Reparatur"), ("STORAGE", "Lagerung"), ("ISSUED", "Ausgegeben")], blank=True)),
("to_stage", models.CharField(max_length=12, choices=[("INTAKE", "Eingang"), ("INSPECTION", "Inspektion"), ("REPAIR", "Reparatur"), ("STORAGE", "Lagerung"), ("ISSUED", "Ausgegeben")])),
("note", models.CharField(max_length=500, blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("device", models.ForeignKey(to="inventory.device", on_delete=django.db.models.deletion.CASCADE, related_name="workflow_events")),
("user", models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=django.db.models.deletion.SET_NULL, null=True)),
],
options={"ordering": ["-created_at", "-id"]},
),
migrations.RunPython(initial_stages, migrations.RunPython.noop),
]

View File

@@ -0,0 +1,36 @@
from django.db import migrations
COMPUTER = {"cpu", "hwid", "ram_gb", "ssd_gb", "hdd_gb", "display_inches", "battery", "wifi", "lan"}
ALLOWED = {
"laptop": COMPUTER,
"aio": COMPUTER - {"battery"},
"desktop": COMPUTER - {"battery", "display_inches"},
"bildschirm": {"display_inches"},
"drucker": {"wifi", "lan"},
}
def normalize(apps, schema_editor):
Device = apps.get_model("inventory", "Device")
for device in Device.objects.select_related("device_type").iterator():
if device.device_type is None:
continue
fields = COMPUTER - ALLOWED.get(device.device_type.label.strip().casefold(), COMPUTER)
if not fields:
continue
changes = {}
legacy = dict((device.custom_values or {}).get("legacy_hardware") or {})
for field in fields:
value = getattr(device, field)
if value not in (None, "", "UNKNOWN"):
legacy[field] = str(value)
changes[field] = None if Device._meta.get_field(field).null else "UNKNOWN" if field in {"battery", "wifi", "lan"} else ""
if legacy:
changes["custom_values"] = {**(device.custom_values or {}), "legacy_hardware": legacy}
Device.objects.filter(pk=device.pk).update(**changes)
class Migration(migrations.Migration):
dependencies = [("inventory", "0007_workflow_tasks")]
operations = [migrations.RunPython(normalize, migrations.RunPython.noop)]

View File

@@ -0,0 +1,237 @@
# Generated by Django 5.2.17 on 2026-09-26 10:54
import django.db.models.deletion
from django.db import migrations, models
def copy_to_specifications(apps, schema_editor):
Device = apps.get_model("inventory", "Device")
Computer = apps.get_model("inventory", "ComputerSpecification")
Monitor = apps.get_model("inventory", "MonitorSpecification")
Printer = apps.get_model("inventory", "PrinterSpecification")
for device in Device.objects.select_related("device_type").iterator(chunk_size=500):
label = device.device_type.label.strip().casefold() if device.device_type else ""
if label == "drucker":
Printer.objects.create(device_id=device.pk, wifi=device.wifi, lan=device.lan)
elif label == "bildschirm":
Monitor.objects.create(device_id=device.pk, display_inches=device.display_inches)
else:
Computer.objects.create(
device_id=device.pk, cpu=device.cpu, hwid=device.hwid,
ram_gb=device.ram_gb, ssd_gb=device.ssd_gb, hdd_gb=device.hdd_gb,
display_inches=device.display_inches, battery=device.battery,
wifi=device.wifi, lan=device.lan,
)
def copy_back_to_devices(apps, schema_editor):
Device = apps.get_model("inventory", "Device")
Computer = apps.get_model("inventory", "ComputerSpecification")
Monitor = apps.get_model("inventory", "MonitorSpecification")
Printer = apps.get_model("inventory", "PrinterSpecification")
for device in Device.objects.select_related("device_type").iterator(chunk_size=500):
label = device.device_type.label.strip().casefold() if device.device_type else ""
if label == "drucker":
spec = Printer.objects.filter(device_id=device.pk).first()
elif label == "bildschirm":
spec = Monitor.objects.filter(device_id=device.pk).first()
else:
spec = Computer.objects.filter(device_id=device.pk).first()
if spec:
Device.objects.filter(pk=device.pk).update(**{
name: getattr(spec, name) for name in (
"cpu", "hwid", "ram_gb", "ssd_gb", "hdd_gb", "display_inches", "battery", "wifi", "lan",
) if hasattr(spec, name)
})
def computer_search_index(apps, schema_editor):
if schema_editor.connection.vendor == "postgresql":
schema_editor.execute(
"CREATE INDEX IF NOT EXISTS inventory_computer_cpu_trgm "
"ON inventory_computerspecification USING gin (cpu gin_trgm_ops)"
)
def drop_computer_search_index(apps, schema_editor):
if schema_editor.connection.vendor == "postgresql":
schema_editor.execute("DROP INDEX IF EXISTS inventory_computer_cpu_trgm")
class Migration(migrations.Migration):
dependencies = [
("inventory", "0008_normalize_hardware_by_type"),
]
operations = [
migrations.CreateModel(
name="ComputerSpecification",
fields=[
(
"device",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="computer_specification",
serialize=False,
to="inventory.device",
),
),
("cpu", models.CharField(blank=True, db_index=True, max_length=200)),
("hwid", models.CharField(blank=True, db_index=True, max_length=255)),
(
"ram_gb",
models.PositiveIntegerField(blank=True, db_index=True, null=True),
),
(
"ssd_gb",
models.PositiveIntegerField(blank=True, db_index=True, null=True),
),
(
"hdd_gb",
models.PositiveIntegerField(blank=True, db_index=True, null=True),
),
(
"display_inches",
models.DecimalField(
blank=True, decimal_places=1, max_digits=4, null=True
),
),
(
"battery",
models.CharField(
choices=[
("YES", "JA"),
("NO", "NEIN"),
("UNKNOWN", "UNBEKANNT"),
],
default="UNKNOWN",
max_length=10,
),
),
(
"wifi",
models.CharField(
choices=[
("YES", "JA"),
("NO", "NEIN"),
("UNKNOWN", "UNBEKANNT"),
],
default="UNKNOWN",
max_length=10,
),
),
(
"lan",
models.CharField(
choices=[
("YES", "JA"),
("NO", "NEIN"),
("UNKNOWN", "UNBEKANNT"),
],
default="UNKNOWN",
max_length=10,
),
),
],
),
migrations.CreateModel(
name="MonitorSpecification",
fields=[
(
"device",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="monitor_specification",
serialize=False,
to="inventory.device",
),
),
(
"display_inches",
models.DecimalField(
blank=True, decimal_places=1, max_digits=4, null=True
),
),
],
),
migrations.CreateModel(
name="PrinterSpecification",
fields=[
(
"device",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="printer_specification",
serialize=False,
to="inventory.device",
),
),
(
"wifi",
models.CharField(
choices=[
("YES", "JA"),
("NO", "NEIN"),
("UNKNOWN", "UNBEKANNT"),
],
default="UNKNOWN",
max_length=10,
),
),
(
"lan",
models.CharField(
choices=[
("YES", "JA"),
("NO", "NEIN"),
("UNKNOWN", "UNBEKANNT"),
],
default="UNKNOWN",
max_length=10,
),
),
],
),
migrations.RunPython(copy_to_specifications, copy_back_to_devices),
migrations.RemoveField(
model_name="device",
name="battery",
),
migrations.RemoveField(
model_name="device",
name="cpu",
),
migrations.RemoveField(
model_name="device",
name="display_inches",
),
migrations.RemoveField(
model_name="device",
name="hdd_gb",
),
migrations.RemoveField(
model_name="device",
name="hwid",
),
migrations.RemoveField(
model_name="device",
name="lan",
),
migrations.RemoveField(
model_name="device",
name="ram_gb",
),
migrations.RemoveField(
model_name="device",
name="ssd_gb",
),
migrations.RemoveField(
model_name="device",
name="wifi",
),
migrations.RunPython(computer_search_index, drop_computer_search_index),
]

View File

@@ -0,0 +1,63 @@
# Generated by Django 5.2.17 on 2026-09-26 11:04
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("inventory", "0009_split_device_specifications"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddIndex(
model_name="devicetask",
index=models.Index(
fields=["state", "due_date"], name="inventory_d_state_bfd57d_idx"
),
),
migrations.AddIndex(
model_name="devicetask",
index=models.Index(
fields=["device", "state"], name="inventory_d_device__f5a9da_idx"
),
),
migrations.AddIndex(
model_name="workflowevent",
index=models.Index(
fields=["device", "-created_at"], name="inventory_w_device__f6b3a5_idx"
),
),
migrations.AddConstraint(
model_name="device",
constraint=models.CheckConstraint(
condition=models.Q(
(
"stage__in",
["INTAKE", "INSPECTION", "REPAIR", "STORAGE", "ISSUED"],
)
),
name="valid_device_stage",
),
),
migrations.AddConstraint(
model_name="devicetask",
constraint=models.CheckConstraint(
condition=models.Q(("state__in", ["OPEN", "DONE"])),
name="valid_device_task_state",
),
),
migrations.AddConstraint(
model_name="devicetask",
constraint=models.CheckConstraint(
condition=models.Q(
models.Q(("completed_at__isnull", True), ("state", "OPEN")),
models.Q(("completed_at__isnull", False), ("state", "DONE")),
_connector="OR",
),
name="device_task_completion_matches_state",
),
),
]

View File

@@ -0,0 +1,98 @@
# Generated by Django 5.2.17 on 2026-09-26 12:11
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("inventory", "0010_workflow_integrity"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="InspectionRecord",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("defects", models.TextField()),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"device",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="inspections",
to="inventory.device",
),
),
(
"recorded_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-created_at", "-id"],
"indexes": [
models.Index(
fields=["device", "-created_at"],
name="inventory_i_device__4a3af8_idx",
)
],
},
),
migrations.CreateModel(
name="RepairRecord",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("work_done", models.TextField()),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"device",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="repairs",
to="inventory.device",
),
),
(
"recorded_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-created_at", "-id"],
"indexes": [
models.Index(
fields=["device", "-created_at"],
name="inventory_r_device__58bce5_idx",
)
],
},
),
]

View File

@@ -0,0 +1,302 @@
# Generated by Django 5.2.17 on 2026-09-28 19:27
import django.db.models.deletion
from django.db import migrations, models
TYPE_TABLES = {
"laptop": "LaptopSpecification",
"desktop": "DesktopSpecification",
"aio": "AllInOneSpecification",
"all-in-one": "AllInOneSpecification",
"all in one": "AllInOneSpecification",
}
def move_computer_rows(apps, schema_editor):
Computer = apps.get_model("inventory", "ComputerSpecification")
targets = {name: apps.get_model("inventory", name) for name in set(TYPE_TABLES.values())}
for source in Computer.objects.select_related("device__device_type").iterator(chunk_size=500):
label = source.device.device_type.label.strip().casefold() if source.device.device_type else ""
target_name = TYPE_TABLES.get(label)
if not target_name:
continue
target = targets[target_name]
fields = [field.name for field in target._meta.fields if field.name != "device"]
target.objects.create(device_id=source.device_id, **{name: getattr(source, name) for name in fields})
source.delete()
def restore_computer_rows(apps, schema_editor):
Computer = apps.get_model("inventory", "ComputerSpecification")
for name in set(TYPE_TABLES.values()):
target = apps.get_model("inventory", name)
for source in target.objects.iterator(chunk_size=500):
fields = [field.name for field in Computer._meta.fields if field.name != "device"]
Computer.objects.create(device_id=source.device_id, **{
field: getattr(source, field) for field in fields if hasattr(source, field)
})
def create_cpu_indexes(apps, schema_editor):
if schema_editor.connection.vendor == "postgresql":
for table, name in (("inventory_laptopspecification", "inventory_laptop_cpu_trgm"),
("inventory_desktopspecification", "inventory_desktop_cpu_trgm"),
("inventory_allinonespecification", "inventory_aio_cpu_trgm")):
schema_editor.execute(f"CREATE INDEX {name} ON {table} USING gin (cpu gin_trgm_ops)")
def drop_cpu_indexes(apps, schema_editor):
if schema_editor.connection.vendor == "postgresql":
for name in ("inventory_laptop_cpu_trgm", "inventory_desktop_cpu_trgm", "inventory_aio_cpu_trgm"):
schema_editor.execute(f"DROP INDEX IF EXISTS {name}")
class Migration(migrations.Migration):
dependencies = [
("inventory", "0011_inspectionrecord_repairrecord"),
]
operations = [
migrations.CreateModel(
name="AllInOneSpecification",
fields=[
("cpu", models.CharField(blank=True, db_index=True, max_length=200)),
("hwid", models.CharField(blank=True, db_index=True, max_length=255)),
(
"ram_gb",
models.PositiveIntegerField(blank=True, db_index=True, null=True),
),
(
"ssd_gb",
models.PositiveIntegerField(blank=True, db_index=True, null=True),
),
(
"hdd_gb",
models.PositiveIntegerField(blank=True, db_index=True, null=True),
),
(
"wifi",
models.CharField(
choices=[
("YES", "JA"),
("NO", "NEIN"),
("UNKNOWN", "UNBEKANNT"),
],
default="UNKNOWN",
max_length=10,
),
),
(
"lan",
models.CharField(
choices=[
("YES", "JA"),
("NO", "NEIN"),
("UNKNOWN", "UNBEKANNT"),
],
default="UNKNOWN",
max_length=10,
),
),
("ports", models.CharField(blank=True, max_length=255)),
(
"device",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="aio_specification",
serialize=False,
to="inventory.device",
),
),
(
"display_inches",
models.DecimalField(
blank=True, decimal_places=1, max_digits=4, null=True
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="DesktopSpecification",
fields=[
("cpu", models.CharField(blank=True, db_index=True, max_length=200)),
("hwid", models.CharField(blank=True, db_index=True, max_length=255)),
(
"ram_gb",
models.PositiveIntegerField(blank=True, db_index=True, null=True),
),
(
"ssd_gb",
models.PositiveIntegerField(blank=True, db_index=True, null=True),
),
(
"hdd_gb",
models.PositiveIntegerField(blank=True, db_index=True, null=True),
),
(
"wifi",
models.CharField(
choices=[
("YES", "JA"),
("NO", "NEIN"),
("UNKNOWN", "UNBEKANNT"),
],
default="UNKNOWN",
max_length=10,
),
),
(
"lan",
models.CharField(
choices=[
("YES", "JA"),
("NO", "NEIN"),
("UNKNOWN", "UNBEKANNT"),
],
default="UNKNOWN",
max_length=10,
),
),
("ports", models.CharField(blank=True, max_length=255)),
(
"device",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="desktop_specification",
serialize=False,
to="inventory.device",
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="LaptopSpecification",
fields=[
("cpu", models.CharField(blank=True, db_index=True, max_length=200)),
("hwid", models.CharField(blank=True, db_index=True, max_length=255)),
(
"ram_gb",
models.PositiveIntegerField(blank=True, db_index=True, null=True),
),
(
"ssd_gb",
models.PositiveIntegerField(blank=True, db_index=True, null=True),
),
(
"hdd_gb",
models.PositiveIntegerField(blank=True, db_index=True, null=True),
),
(
"wifi",
models.CharField(
choices=[
("YES", "JA"),
("NO", "NEIN"),
("UNKNOWN", "UNBEKANNT"),
],
default="UNKNOWN",
max_length=10,
),
),
(
"lan",
models.CharField(
choices=[
("YES", "JA"),
("NO", "NEIN"),
("UNKNOWN", "UNBEKANNT"),
],
default="UNKNOWN",
max_length=10,
),
),
("ports", models.CharField(blank=True, max_length=255)),
(
"device",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="laptop_specification",
serialize=False,
to="inventory.device",
),
),
(
"display_inches",
models.DecimalField(
blank=True, decimal_places=1, max_digits=4, null=True
),
),
(
"battery",
models.CharField(
choices=[
("YES", "JA"),
("NO", "NEIN"),
("UNKNOWN", "UNBEKANNT"),
],
default="UNKNOWN",
max_length=10,
),
),
],
options={
"abstract": False,
},
),
migrations.AddField(
model_name="computerspecification",
name="ports",
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name="monitorspecification",
name="ports",
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name="monitorspecification",
name="resolution",
field=models.CharField(blank=True, max_length=80),
),
migrations.AddField(
model_name="printerspecification",
name="color_printing",
field=models.CharField(
choices=[("YES", "JA"), ("NO", "NEIN"), ("UNKNOWN", "UNBEKANNT")],
default="UNKNOWN",
max_length=10,
),
),
migrations.AddField(
model_name="printerspecification",
name="duplex",
field=models.CharField(
choices=[("YES", "JA"), ("NO", "NEIN"), ("UNKNOWN", "UNBEKANNT")],
default="UNKNOWN",
max_length=10,
),
),
migrations.AddField(
model_name="printerspecification",
name="ports",
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name="printerspecification",
name="print_technology",
field=models.CharField(blank=True, max_length=80),
),
migrations.RunPython(move_computer_rows, restore_computer_rows),
migrations.RunPython(create_cpu_indexes, drop_cpu_indexes),
]

View File