Initialize clean CSR inventory repository
This commit is contained in:
146
inventory/workflow.py
Normal file
146
inventory/workflow.py
Normal 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
|
||||
Reference in New Issue
Block a user