from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.http import QueryDict from django.test import TestCase from django.urls import reverse from .device_types import applicable_fields from .api import DeviceSerializer from .forms import DeviceForm from .models import AllInOneSpecification, ChoiceValue, ComputerSpecification, DesktopSpecification, Device, DeviceTask, InspectionRecord, LaptopSpecification, MonitorSpecification, PrinterSpecification, RepairRecord, UserProfile, WorkflowEvent from .query import device_queryset from .services import update_device_field from .workflow import advance_device, complete_task, create_task class WorkflowTests(TestCase): @classmethod def setUpTestData(cls): cls.status, _ = ChoiceValue.objects.get_or_create(kind="status", label="Eingang") for label in ("Defekt", "Fertig", "Ausgegeben"): ChoiceValue.objects.get_or_create(kind="status", label=label) cls.laptop, _ = ChoiceValue.objects.get_or_create(kind="device_type", label="Laptop") cls.printer, _ = ChoiceValue.objects.get_or_create(kind="device_type", label="Drucker") cls.user = User.objects.create_user("editor", password="test-password") cls.user.profile.role = UserProfile.Role.EDITOR cls.user.profile.save() cls.viewer = User.objects.create_user("viewer", password="test-password") def setUp(self): self.device = Device.objects.create(status=self.status, device_type=self.laptop) def test_flow_blocks_open_tasks_and_requires_recipient_for_issue(self): with self.assertRaises(ValidationError): advance_device(self.device.pk, Device.Stage.ISSUED, user=self.user, expected_revision=self.device.revision) self.device = advance_device(self.device.pk, Device.Stage.INSPECTION, user=self.user, expected_revision=self.device.revision) task = create_task(self.device.pk, title="Akku prüfen", user=self.user) with self.assertRaises(ValidationError): advance_device(self.device.pk, Device.Stage.STORAGE, user=self.user, expected_revision=self.device.revision) complete_task(self.device.pk, task.pk, user=self.user) self.device = advance_device(self.device.pk, Device.Stage.STORAGE, user=self.user, expected_revision=self.device.revision) with self.assertRaises(ValidationError): advance_device(self.device.pk, Device.Stage.ISSUED, user=self.user, expected_revision=self.device.revision) recipient = ChoiceValue.objects.create(kind="recipient", label="Verein") self.device.recipient = recipient self.device.save() self.device = advance_device(self.device.pk, Device.Stage.ISSUED, user=self.user, expected_revision=self.device.revision, note="Abgeholt") self.assertEqual(WorkflowEvent.objects.filter(device=self.device).count(), 3) self.assertEqual(self.device.tasks.get().state, DeviceTask.State.DONE) self.assertEqual(self.device.workflow_events.first().note, "Abgeholt") self.assertEqual(self.device.status.label, "Ausgegeben") def test_stale_transition_and_viewer_cannot_change_workflow(self): self.client.force_login(self.viewer) response = self.client.post(reverse("inventory:device-advance", args=[self.device.pk]), {"stage": "INSPECTION", "expected_revision": self.device.revision}) self.assertEqual(response.status_code, 403) self.assertFalse(WorkflowEvent.objects.exists()) with self.assertRaises(ValidationError): advance_device(self.device.pk, Device.Stage.INSPECTION, user=self.user, expected_revision=self.device.revision + 1) def test_printer_has_no_computer_fields_in_form_and_inline_write_is_rejected(self): self.assertNotIn("ram_gb", applicable_fields(self.printer)) self.device.device_type = self.printer self.device.save() form = DeviceForm(instance=self.device) self.assertNotIn("ram_gb", applicable_fields(self.printer)) self.assertIn("ram_gb", form.fields) with self.assertRaises(ValidationError): update_device_field(self.device, "ram_gb", "8", self.user) self.client.force_login(self.user) response = self.client.get(reverse("inventory:device-detail", args=[self.device.pk])) self.assertEqual(response.status_code, 200) self.assertContains(response, 'data-hardware-field="ram_gb" hidden') self.assertContains(response, "Aufgaben") listing = self.client.get(reverse("inventory:device-list"), {"type": self.printer.pk}) self.assertEqual(listing.status_code, 200) self.assertNotContains(listing, 'data-column="ram_gb"') def test_task_board_and_form_work_for_editor(self): self.client.force_login(self.user) response = self.client.post(reverse("inventory:device-task-add", args=[self.device.pk]), { "title": "Gehäuse prüfen", "due_date": "2026-10-01", "assigned_to": self.user.pk, }) self.assertEqual(response.status_code, 302) task = DeviceTask.objects.get(device=self.device) self.assertEqual(task.assigned_to, self.user) response = self.client.get(reverse("inventory:task-list")) self.assertContains(response, "Gehäuse prüfen") self.client.post(reverse("inventory:device-task-complete", args=[self.device.pk, task.pk])) task.refresh_from_db() self.assertEqual(task.state, DeviceTask.State.DONE) def test_changing_to_printer_preserves_old_hardware_in_legacy_data(self): self.device.ram_gb = 16 self.device.cpu = "Core i5" self.device.save() update_device_field(self.device, "device_type", str(self.printer.pk), self.user) self.device.refresh_from_db() self.assertIsNone(self.device.ram_gb) self.assertEqual(self.device.cpu, "") self.assertEqual(self.device.custom_values["legacy_hardware"]["ram_gb"], "16") self.assertFalse(LaptopSpecification.objects.filter(device=self.device).exists()) self.assertTrue(PrinterSpecification.objects.filter(device=self.device).exists()) def test_detail_form_switches_type_and_saves_only_printer_fields(self): self.device.ram_gb = 16 self.device.save() self.client.force_login(self.user) response = self.client.post(reverse("inventory:device-detail", args=[self.device.pk]), { "expected_revision": self.device.revision, "status": self.status.pk, "device_type": self.printer.pk, "date": "28.09.2026", "print_technology": "Laser", "duplex": "YES", "color_printing": "NO", "ports": "USB", "ram_gb": "999", }) self.assertEqual(response.status_code, 302) self.device.refresh_from_db() self.assertEqual(self.device.device_type, self.printer) self.assertEqual(self.device.print_technology, "Laser") self.assertEqual(self.device.ports, "USB") self.assertIsNone(self.device.ram_gb) self.assertEqual(self.device.custom_values["legacy_hardware"]["ram_gb"], "16") response = self.client.post(reverse("inventory:device-detail", args=[self.device.pk]), { "expected_revision": self.device.revision, "status": "", "device_type": self.laptop.pk, "ram_gb": "32", }) self.assertEqual(response.status_code, 200) self.assertContains(response, 'data-hardware-field="ram_gb"') self.assertEqual(set(response.context["hardware_fields"]), applicable_fields(self.laptop)) def test_type_specific_tables_and_direct_workflow_board(self): self.device.ram_gb = 8 self.device.save(update_fields=["ram_gb"]) self.assertEqual(LaptopSpecification.objects.get(device=self.device).ram_gb, 8) printer = Device.objects.create(status=self.status, device_type=self.printer, wifi=Device.TriState.YES, print_technology="Laser", duplex=Device.TriState.YES) self.assertTrue(PrinterSpecification.objects.filter(device=printer).exists()) self.assertEqual(PrinterSpecification.objects.get(device=printer).print_technology, "Laser") self.assertFalse(ComputerSpecification.objects.filter(device=printer).exists()) screen, _ = ChoiceValue.objects.get_or_create(kind="device_type", label="Bildschirm") monitor = Device.objects.create(status=self.status, device_type=screen, display_inches="24.0", resolution="1920x1080", ports="HDMI, DisplayPort") self.assertTrue(MonitorSpecification.objects.filter(device=monitor).exists()) self.assertEqual(MonitorSpecification.objects.get(device=monitor).ports, "HDMI, DisplayPort") self.assertFalse(ComputerSpecification.objects.filter(device=monitor).exists()) self.assertIn("resolution", DeviceForm(instance=monitor).fields) self.assertIn("ports", DeviceForm(instance=monitor).fields) self.assertNotIn("ram_gb", applicable_fields(screen)) self.assertIn("print_technology", DeviceForm(instance=printer).fields) self.assertEqual(DeviceSerializer(monitor).data["ports"], "HDMI, DisplayPort") desktop_type, _ = ChoiceValue.objects.get_or_create(kind="device_type", label="Desktop") desktop = Device.objects.create(status=self.status, device_type=desktop_type, ram_gb=32, ports="USB-C, HDMI") self.assertEqual(DesktopSpecification.objects.get(device=desktop).ports, "USB-C, HDMI") self.assertIn(desktop, device_queryset(QueryDict("ram_min=32"))) self.assertNotIn(self.device, device_queryset(QueryDict("ram_min=32"))) aio_type, _ = ChoiceValue.objects.get_or_create(kind="device_type", label="AiO") aio = Device.objects.create(status=self.status, device_type=aio_type, display_inches="27.0") self.assertEqual(str(AllInOneSpecification.objects.get(device=aio).display_inches), "27.0") self.client.force_login(self.user) board = self.client.get(reverse("inventory:workflow-board")) self.assertContains(board, f"#{self.device.number}") self.assertContains(board, "Neue Aufgabe") listing = self.client.get(reverse("inventory:device-list")) self.assertContains(listing, reverse("inventory:device-detail", args=[self.device.pk])) def test_board_quick_actions_return_to_board(self): self.client.force_login(self.user) board_url = reverse("inventory:workflow-board") response = self.client.post(reverse("inventory:device-task-add", args=[self.device.pk]), { "title": "Gerät prüfen", "return_to": "workflow", }) self.assertRedirects(response, board_url) task = DeviceTask.objects.get(device=self.device) self.assertEqual(task.title, "Gerät prüfen") response = self.client.post(reverse("inventory:device-advance", args=[self.device.pk]), { "stage": Device.Stage.INSPECTION, "expected_revision": self.device.revision, "return_to": "workflow", }) self.assertRedirects(response, board_url) self.device.refresh_from_db() self.assertEqual(self.device.stage, Device.Stage.INSPECTION) board = self.client.get(board_url) self.assertContains(board, "Gerät prüfen") self.assertContains(board, f"#{self.device.number}") def test_stage_forms_capture_inspection_repair_and_recipient(self): self.client.force_login(self.user) self.device = advance_device(self.device.pk, Device.Stage.INSPECTION, user=self.user, expected_revision=self.device.revision) board_url = reverse("inventory:workflow-board") board = self.client.get(board_url) self.assertContains(board, "Festgestellte Mängel") self.assertContains(board, 'name="ram_gb"') inspection_url = reverse("inventory:device-inspection-save", args=[self.device.pk]) response = self.client.post(inspection_url, { "defects": "Akku defekt", "model": "ThinkPad", "cpu": "Core i5", "ram_gb": "16", "ssd_gb": "512", "battery": "NO", "wifi": "YES", "lan": "YES", "expected_revision": self.device.revision, "return_to": "workflow", }) self.assertRedirects(response, board_url) self.device.refresh_from_db() self.assertEqual(self.device.ram_gb, 16) self.assertEqual(self.device.model, "ThinkPad") self.assertEqual(InspectionRecord.objects.get(device=self.device).defects, "Akku defekt") stale = self.client.post(inspection_url, { "defects": "Falscher Befund", "battery": "UNKNOWN", "wifi": "UNKNOWN", "lan": "UNKNOWN", "expected_revision": self.device.revision - 1, }) self.assertEqual(stale.status_code, 302) self.assertEqual(InspectionRecord.objects.count(), 1) self.device = advance_device(self.device.pk, Device.Stage.REPAIR, user=self.user, expected_revision=self.device.revision) response = self.client.post(reverse("inventory:device-repair-add", args=[self.device.pk]), { "work_done": "Akku ersetzt", "expected_revision": self.device.revision, "return_to": "workflow", }) self.assertRedirects(response, board_url) self.assertEqual(RepairRecord.objects.get(device=self.device).work_done, "Akku ersetzt") self.device.refresh_from_db() self.device = advance_device(self.device.pk, Device.Stage.STORAGE, user=self.user, expected_revision=self.device.revision) response = self.client.post(reverse("inventory:device-storage-assign", args=[self.device.pk]), { "recipient": "Verein Nord", "expected_revision": self.device.revision, "return_to": "workflow", }) self.assertRedirects(response, board_url) self.device.refresh_from_db() self.assertEqual(self.device.recipient.label, "Verein Nord") board = self.client.get(board_url) self.assertContains(board, "Verein Nord") def test_inspection_respects_device_type_and_viewer_is_read_only(self): printer = Device.objects.create(status=self.status, device_type=self.printer) printer = advance_device(printer.pk, Device.Stage.INSPECTION, user=self.user, expected_revision=printer.revision) self.client.force_login(self.user) board = self.client.get(reverse("inventory:workflow-board")) self.assertContains(board, "Festgestellte Mängel") self.assertNotContains(board, 'name="ram_gb"') response = self.client.post(reverse("inventory:device-inspection-save", args=[printer.pk]), { "defects": "Papierstau", "ram_gb": "64", "wifi": "YES", "lan": "NO", "duplex": "UNKNOWN", "color_printing": "UNKNOWN", "expected_revision": printer.revision, }) self.assertEqual(response.status_code, 302) printer.refresh_from_db() self.assertIsNone(printer.ram_gb) self.assertEqual(printer.wifi, Device.TriState.YES) self.client.force_login(self.viewer) response = self.client.post(reverse("inventory:device-repair-add", args=[printer.pk]), { "work_done": "Unberechtigt", "expected_revision": printer.revision, }) self.assertEqual(response.status_code, 403) self.assertFalse(RepairRecord.objects.exists())