"""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