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

146
inventory/workflow.py Normal file
View File

@@ -0,0 +1,146 @@
"""Validated workflow and task operations. All writes use row locks."""
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils import timezone
from .models import AuditLog, ChoiceValue, Device, DeviceTask, InspectionRecord, RepairRecord, WorkflowEvent
from .services import add_audit
from .device_types import applicable_fields
TRANSITIONS = {
Device.Stage.INTAKE: {Device.Stage.INSPECTION},
Device.Stage.INSPECTION: {Device.Stage.REPAIR, Device.Stage.STORAGE},
Device.Stage.REPAIR: {Device.Stage.INSPECTION, Device.Stage.STORAGE},
Device.Stage.STORAGE: {Device.Stage.REPAIR, Device.Stage.ISSUED},
Device.Stage.ISSUED: {Device.Stage.INTAKE},
}
STATUS_BY_STAGE = {
Device.Stage.INTAKE: "Eingang",
Device.Stage.REPAIR: "Defekt",
Device.Stage.STORAGE: "Fertig",
Device.Stage.ISSUED: "Ausgegeben",
}
def _locked_stage_device(device_id, stage, expected_revision):
device = Device.objects.select_for_update().get(pk=device_id, archived_at__isnull=True)
if expected_revision is None or str(expected_revision) != str(device.revision):
raise ValidationError("Das Gerät wurde inzwischen geändert. Bitte neu laden.")
if device.stage != stage:
raise ValidationError("Dieses Formular gehört nicht zum aktuellen Arbeitsschritt.")
return device
@transaction.atomic
def save_inspection(device_id, values, *, user, expected_revision):
device = _locked_stage_device(device_id, Device.Stage.INSPECTION, expected_revision)
defects = values["defects"].strip()
if not defects or len(defects) > 4000:
raise ValidationError("Bitte festgestellte Mängel eintragen (maximal 4000 Zeichen).")
changed = ["updated_at"]
for name in ("model", *applicable_fields(device.device_type)):
if name not in values:
continue
old, new = getattr(device, name), values[name]
if old != new:
setattr(device, name, new)
changed.append(name)
add_audit(user, device, AuditLog.Action.UPDATE, name, old, new, field_key=name)
device.save(update_fields=changed)
InspectionRecord.objects.create(device=device, defects=defects, recorded_by=user)
add_audit(user, device, AuditLog.Action.UPDATE, "Inspektion", "", defects)
return device
@transaction.atomic
def record_repair(device_id, work_done, *, user, expected_revision):
device = _locked_stage_device(device_id, Device.Stage.REPAIR, expected_revision)
work_done = work_done.strip()
if not work_done or len(work_done) > 4000:
raise ValidationError("Bitte die durchgeführten Arbeiten eintragen (maximal 4000 Zeichen).")
RepairRecord.objects.create(device=device, work_done=work_done, recorded_by=user)
device.save(update_fields=["updated_at"])
add_audit(user, device, AuditLog.Action.UPDATE, "Reparatur", "", work_done)
return device
@transaction.atomic
def assign_storage_recipient(device_id, recipient_name, *, user, expected_revision):
device = _locked_stage_device(device_id, Device.Stage.STORAGE, expected_revision)
recipient_name = recipient_name.strip()
if not recipient_name or len(recipient_name) > 120:
raise ValidationError("Bitte einen Empfänger mit höchstens 120 Zeichen eintragen.")
recipient = ChoiceValue.objects.filter(kind=ChoiceValue.Kind.RECIPIENT, label__iexact=recipient_name).first()
if recipient is None:
recipient = ChoiceValue.objects.create(kind=ChoiceValue.Kind.RECIPIENT, label=recipient_name)
old = device.recipient
if old != recipient:
device.recipient = recipient
device.save(update_fields=["recipient"])
add_audit(user, device, AuditLog.Action.UPDATE, "Empfänger", old, recipient,
field_key="recipient", old_data=old.pk if old else None, new_data=recipient.pk)
return device
@transaction.atomic
def advance_device(device_id, target, *, user, note="", expected_revision=None):
device = Device.objects.select_for_update().get(pk=device_id, archived_at__isnull=True)
if expected_revision is None or str(expected_revision) != str(device.revision):
raise ValidationError("Das Gerät wurde inzwischen geändert. Bitte neu laden.")
if target not in TRANSITIONS[device.stage]:
raise ValidationError("Dieser Schritt ist vom aktuellen Zustand nicht erlaubt.")
if target in {Device.Stage.STORAGE, Device.Stage.ISSUED} and device.tasks.filter(state=DeviceTask.State.OPEN).exists():
raise ValidationError("Vor der Lagerung oder Ausgabe müssen alle Aufgaben erledigt sein.")
if target == Device.Stage.ISSUED and not device.recipient_id:
raise ValidationError("Vor der Ausgabe einen Empfänger eintragen.")
if len(note) > 500:
raise ValidationError("Die Notiz darf höchstens 500 Zeichen enthalten.")
previous = device.stage
device.stage = target
update_fields = ["stage"]
desired_label = STATUS_BY_STAGE.get(target)
old_status = device.status
desired_status = ChoiceValue.objects.filter(
kind=ChoiceValue.Kind.STATUS, label=desired_label, active=True,
).first() if desired_label else None
if desired_status and desired_status.pk != device.status_id:
device.status = desired_status
update_fields.append("status")
device.save(update_fields=update_fields)
WorkflowEvent.objects.create(device=device, from_stage=previous, to_stage=target, note=note, user=user)
add_audit(user, device, AuditLog.Action.UPDATE, "Arbeitsschritt", previous, target,
field_key="stage", old_data=previous, new_data=target)
if "status" in update_fields:
add_audit(user, device, AuditLog.Action.UPDATE, "Status", old_status, desired_status,
field_key="status", old_data=old_status.pk, new_data=desired_status.pk)
return device
@transaction.atomic
def create_task(device_id, *, title, description="", due_date=None, assigned_to=None, user):
device = Device.objects.select_for_update().get(pk=device_id, archived_at__isnull=True)
title = title.strip()
if not title or len(title) > 200:
raise ValidationError("Bitte einen Aufgabentitel mit höchstens 200 Zeichen eingeben.")
if device.stage == Device.Stage.ISSUED:
raise ValidationError("Für ausgegebene Geräte können keine Aufgaben angelegt werden.")
task = DeviceTask.objects.create(device=device, title=title, description=description.strip(),
due_date=due_date, assigned_to=assigned_to, created_by=user)
add_audit(user, device, AuditLog.Action.UPDATE, "Aufgabe angelegt", "", title)
return task
@transaction.atomic
def complete_task(device_id, task_id, *, user):
device = Device.objects.select_for_update().get(pk=device_id, archived_at__isnull=True)
task = DeviceTask.objects.select_for_update().get(pk=task_id, device=device)
if task.state != DeviceTask.State.OPEN:
raise ValidationError("Diese Aufgabe ist bereits erledigt.")
task.state = DeviceTask.State.DONE
task.completed_by = user
task.completed_at = timezone.now()
task.save(update_fields=["state", "completed_by", "completed_at", "updated_at"])
add_audit(user, device, AuditLog.Action.UPDATE, "Aufgabe erledigt", "", task.title)
return task