"""Тести IT-порталу: доступ через групу IT, інвентаризація обладнання (зі списанням історії видачі), черга заявок, журнал робіт.""" from datetime import date from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.test import Client, TestCase from apps.it_manager.models import ( Equipment, EquipmentAssignment, EquipmentModel, Ticket, WorkLog, ) from core.models.directories import Directory from core.test_utils import login_2fa Employee = get_user_model() _employee_seq = 0 def _dir_id(type_code, code): """Знаходить Directory.pk за (тип, код) — довідники засіяні data-міграціями конверсії choices.""" return Directory.objects.get(directory_type__code=type_code, code=code).id def make_employee(**overrides): global _employee_seq _employee_seq += 1 defaults = { 'username': f'itman.user{_employee_seq}', 'email': f'itman.user{_employee_seq}@company.local', 'first_name': 'Тест', 'last_name': 'Співробітник', 'hire_date': date(2026, 1, 1), 'user_location_type': 'office', 'mattermost_sync_enabled': False, 'ad_sync_enabled': False, } defaults.update(overrides) return Employee.objects.create(**defaults) def make_equipment_model(**overrides): defaults = { 'equipment_type_id': _dir_id('equipment_type', 'laptop'), 'name': 'ThinkPad T14', } defaults.update(overrides) obj, _ = EquipmentModel.objects.get_or_create(**defaults) return obj def make_equipment(**overrides): defaults = { 'equipment_model': make_equipment_model(), 'serial_number': f'SN-{Equipment.objects.count() + 1}', } defaults.update(overrides) return Equipment.objects.create(**defaults) class ItManagerAccessTests(TestCase): """Сторінка й усі мутації доступні лише групі IT (або суперюзеру).""" def setUp(self): self.outsider = make_employee() self.client = Client() self.client.force_login(self.outsider) def test_page_forbidden_for_non_it(self): r = self.client.get('/it-manager/') self.assertEqual(r.status_code, 302) self.assertEqual(r['Location'], '/dashboard/') def test_equipment_create_forbidden_for_non_it(self): r = self.client.post('/it-manager/equipment/bulk-create/', { 'equipment_model_id': '1', 'serial_numbers': 'X1', }) self.assertEqual(r.status_code, 403) def test_worklog_create_forbidden_for_non_it(self): r = self.client.post('/it-manager/worklog/create/', { 'work_type': 'repair', 'description': 'test', }) self.assertEqual(r.status_code, 403) class ItManagerEquipmentTests(TestCase): def setUp(self): self.it_user = make_employee(is_staff=False) self.it_user.groups.add(Group.objects.get(name='IT')) self.client = Client() login_2fa(self.client, self.it_user) def test_page_accessible_for_it_group(self): r = self.client.get('/it-manager/') self.assertEqual(r.status_code, 200) def test_bulk_create_equipment(self): model = make_equipment_model() # масовий ввід: одна модель + список серійників (з дублем) r = self.client.post('/it-manager/equipment/bulk-create/', { 'equipment_model_id': str(model.pk), 'serial_numbers': 'SN-100\nSN-101\nSN-101\nSN-102', }, HTTP_HX_REQUEST='true') self.assertEqual(r.status_code, 200) units = Equipment.objects.filter(equipment_model=model) self.assertEqual(units.count(), 3) # дубль SN-101 пропущено self.assertEqual( sorted(units.values_list('serial_number', flat=True)), ['SN-100', 'SN-101', 'SN-102'], ) self.assertTrue(all(u.status.code == 'stock' for u in units)) def test_bulk_assign_equipment(self): model = make_equipment_model() units = [ Equipment.objects.create(equipment_model=model, serial_number=f'BA-{i}') for i in range(3) ] emp = make_employee() r = self.client.post('/it-manager/equipment/bulk-assign/', { 'equipment_ids': [str(units[0].pk), str(units[1].pk)], 'assign_target': 'employee', 'assigned_to_id': str(emp.id), }, HTTP_HX_REQUEST='true') self.assertEqual(r.status_code, 200) units[0].refresh_from_db(); units[1].refresh_from_db(); units[2].refresh_from_db() self.assertEqual(units[0].assigned_to_id, emp.id) self.assertEqual(units[0].status.code, 'assigned') self.assertIsNone(units[2].assigned_to_id) # не відмічений — не чіпаємо self.assertEqual(EquipmentAssignment.objects.filter(equipment__in=units[:2]).count(), 2) def test_assign_equipment_creates_and_closes_history(self): equipment = make_equipment() emp1 = make_employee() emp2 = make_employee() self.client.post(f'/it-manager/equipment/{equipment.id}/update/', { 'status': 'assigned', 'assign_target': 'employee', 'assigned_to_id': emp1.id, }) equipment.refresh_from_db() self.assertEqual(equipment.assigned_to_id, emp1.id) self.assertEqual(equipment.status.code, 'assigned') self.assertEqual(EquipmentAssignment.objects.filter(equipment=equipment, returned_at__isnull=True).count(), 1) self.client.post(f'/it-manager/equipment/{equipment.id}/update/', { 'status': 'assigned', 'assign_target': 'employee', 'assigned_to_id': emp2.id, }) equipment.refresh_from_db() self.assertEqual(equipment.assigned_to_id, emp2.id) first_assignment = EquipmentAssignment.objects.get(equipment=equipment, assigned_to=emp1) self.assertIsNotNone(first_assignment.returned_at) self.assertEqual( EquipmentAssignment.objects.filter(equipment=equipment, assigned_to=emp2, returned_at__isnull=True).count(), 1, ) def test_unassign_equipment_closes_history_without_new_entry(self): equipment = make_equipment() emp1 = make_employee() self.client.post(f'/it-manager/equipment/{equipment.id}/update/', { 'status': 'assigned', 'assign_target': 'employee', 'assigned_to_id': emp1.id, }) self.client.post(f'/it-manager/equipment/{equipment.id}/update/', { 'status': 'stock', 'assign_target': '', }) equipment.refresh_from_db() self.assertIsNone(equipment.assigned_to_id) self.assertEqual(equipment.status.code, 'stock') self.assertEqual(EquipmentAssignment.objects.filter(equipment=equipment, returned_at__isnull=True).count(), 0) def test_bulk_assign_to_location_only(self): from apps.locations.models.base import Location from core.models.directories import DirectoryType region_type, _ = DirectoryType.objects.get_or_create( code='location_region', defaults={'name': 'Регион'}, ) region, _ = Directory.objects.get_or_create( directory_type=region_type, code='eq-reg', defaults={'name': 'Тест'}, ) location = Location.objects.create( address='Аптека тест', location_type='pharmacy', location_region=region, is_active=True, ) model = make_equipment_model(name='RB952Ui') unit = Equipment.objects.create(equipment_model=model, serial_number='RT-LOC-1') r = self.client.post('/it-manager/equipment/bulk-assign/', { 'equipment_ids': [str(unit.pk)], 'assign_target': 'pharmacy', 'location_id': str(location.pk), }, HTTP_HX_REQUEST='true') self.assertEqual(r.status_code, 200) unit.refresh_from_db() self.assertEqual(unit.location_id, location.pk) self.assertIsNone(unit.assigned_to_id) self.assertEqual(unit.status.code, 'assigned') hist = EquipmentAssignment.objects.get(equipment=unit, returned_at__isnull=True) self.assertEqual(hist.location_id, location.pk) self.assertIsNone(hist.assigned_to_id) def test_update_assign_to_pharmacy(self): from apps.locations.models.base import Location from core.models.directories import DirectoryType region_type, _ = DirectoryType.objects.get_or_create( code='location_region', defaults={'name': 'Регион'}, ) region, _ = Directory.objects.get_or_create( directory_type=region_type, code='eq-reg2', defaults={'name': 'Тест2'}, ) location = Location.objects.create( address='А12', location_type='pharmacy', location_region=region, is_active=True, ) equipment = make_equipment(serial_number='RT-UPD-1') self.client.post(f'/it-manager/equipment/{equipment.id}/update/', { 'status': 'assigned', 'assign_target': 'pharmacy', 'location_id': str(location.pk), }) equipment.refresh_from_db() self.assertEqual(equipment.location_id, location.pk) self.assertIsNone(equipment.assigned_to_id) self.assertEqual(equipment.status.code, 'assigned') self.assertEqual( EquipmentAssignment.objects.filter(equipment=equipment, returned_at__isnull=True).count(), 1, ) # повернення на склад self.client.post(f'/it-manager/equipment/{equipment.id}/update/', { 'status': 'stock', 'assign_target': '', }) equipment.refresh_from_db() self.assertIsNone(equipment.location_id) self.assertEqual(equipment.status.code, 'stock') self.assertEqual( EquipmentAssignment.objects.filter(equipment=equipment, returned_at__isnull=True).count(), 0, ) def test_bulk_create_with_location_writes_history(self): from apps.locations.models.base import Location from core.models.directories import DirectoryType region_type, _ = DirectoryType.objects.get_or_create( code='location_region', defaults={'name': 'Регион'}, ) region, _ = Directory.objects.get_or_create( directory_type=region_type, code='eq-reg3', defaults={'name': 'Тест3'}, ) location = Location.objects.create( address='Склад-офіс', location_type='office', location_region=region, is_active=True, ) model = make_equipment_model() r = self.client.post('/it-manager/equipment/bulk-create/', { 'equipment_model_id': str(model.pk), 'serial_numbers': 'SN-LOC-A', 'assign_target': 'office', 'location_id': str(location.pk), }, HTTP_HX_REQUEST='true') self.assertEqual(r.status_code, 200) unit = Equipment.objects.get(serial_number='SN-LOC-A') self.assertEqual(unit.location_id, location.pk) self.assertEqual(unit.status.code, 'assigned') self.assertEqual( EquipmentAssignment.objects.filter(equipment=unit, location=location).count(), 1, ) class ItManagerTicketTests(TestCase): def setUp(self): self.it_user = make_employee(is_staff=False) self.it_user.groups.add(Group.objects.get(name='IT')) self.requester = make_employee() self.client = Client() login_2fa(self.client, self.it_user) def test_resolve_ticket_stamps_resolved_at(self): ticket = Ticket.objects.create( title='Не працює принтер', description='...', ticket_type_id=_dir_id('ticket_type', 'repair'), requester=self.requester, ) self.client.post(f'/it-manager/tickets/{ticket.id}/update/', { 'status': 'resolved', 'assignee_id': self.it_user.id, }) ticket.refresh_from_db() self.assertEqual(ticket.status.code, 'resolved') self.assertEqual(ticket.assignee_id, self.it_user.id) self.assertIsNotNone(ticket.resolved_at) def test_add_comment(self): ticket = Ticket.objects.create( title='Питання', description='...', ticket_type_id=_dir_id('ticket_type', 'consultation'), requester=self.requester, ) r = self.client.post(f'/it-manager/tickets/{ticket.id}/comment/', { 'text': 'Дивимось', 'is_internal': 'on', }) self.assertEqual(r.status_code, 200) self.assertEqual(ticket.comments.count(), 1) self.assertTrue(ticket.comments.first().is_internal) class ItManagerWorkLogTests(TestCase): def setUp(self): self.it_user = make_employee(is_staff=False) self.it_user.groups.add(Group.objects.get(name='IT')) self.client = Client() login_2fa(self.client, self.it_user) def test_create_worklog(self): r = self.client.post('/it-manager/worklog/create/', { 'work_type': 'maintenance', 'description': 'Профілактика серверної', 'hours_spent': '2.5', }) self.assertEqual(r.status_code, 200) log = WorkLog.objects.get(description='Профілактика серверної') self.assertEqual(log.technician_id, self.it_user.id) self.assertEqual(float(log.hours_spent), 2.5) self.assertContains(r, 'Запись о выполненной работе сохранена') def test_create_worklog_requires_description(self): before = WorkLog.objects.count() r = self.client.post('/it-manager/worklog/create/', { 'work_type': 'maintenance', 'description': ' ', }) self.assertEqual(r.status_code, 200) self.assertEqual(WorkLog.objects.count(), before) self.assertContains(r, 'Укажите описание работ') def test_create_worklog_linked_to_ticket(self): from apps.it_manager.models import Ticket from core.models.directories import Directory, DirectoryType ttype, _ = DirectoryType.objects.get_or_create(code='ticket_type', defaults={'name': 'Тип заявки'}) ptype, _ = DirectoryType.objects.get_or_create(code='ticket_priority', defaults={'name': 'Приоритет'}) stype, _ = DirectoryType.objects.get_or_create(code='ticket_status', defaults={'name': 'Статус'}) ticket_type, _ = Directory.objects.get_or_create( directory_type=ttype, code='incident', defaults={'name': 'Инцидент'}, ) priority, _ = Directory.objects.get_or_create( directory_type=ptype, code='medium', defaults={'name': 'Средний'}, ) status, _ = Directory.objects.get_or_create( directory_type=stype, code='in_progress', defaults={'name': 'В работе'}, ) ticket = Ticket.objects.create( title='Не працює каса', ticket_type=ticket_type, priority=priority, status=status, requester=self.it_user, description='Каса не відкривається', ) r = self.client.post('/it-manager/worklog/create/', { 'work_type': 'repair', 'description': 'Перезапуск касового ПЗ', 'ticket_id': ticket.pk, 'hours_spent': '1', }) self.assertEqual(r.status_code, 200) log = WorkLog.objects.get(description='Перезапуск касового ПЗ') self.assertEqual(log.ticket_id, ticket.pk) self.assertContains(r, 'Работа привязана к заявке') self.assertContains(r, ticket.number) class ItManagerWorkLogFilterEditTests(TestCase): """Фильтр по сотруднику и inline-редактирование для руководителя.""" def setUp(self): from core.models.directories import DirectoryType dept_type, _ = DirectoryType.objects.get_or_create( code='department', defaults={'name': 'Відділи', 'hierarchy_type': 'strict'}, ) self.dept = Directory.objects.create( directory_type=dept_type, code='it-wl', name='IT-відділ-wl', ) self.head = make_employee( last_name='Керівник', first_name='ІТ', username='it.head.wl', department=self.dept, user_location_type='office', ) self.tech_a = make_employee( last_name='Альфа', first_name='Тех', username='tech.a.wl', department=self.dept, manager=self.head, user_location_type='office', ) self.tech_b = make_employee( last_name='Бета', first_name='Тех', username='tech.b.wl', department=self.dept, manager=self.head, user_location_type='office', ) it_group = Group.objects.get(name='IT') for u in (self.head, self.tech_a, self.tech_b): u.groups.add(it_group) self.log_a = WorkLog.objects.create( date=date(2026, 9, 1), technician=self.tech_a, work_type_id=_dir_id('worklog_type', 'maintenance'), description='Робота А', hours_spent=1, ) self.log_b = WorkLog.objects.create( date=date(2026, 9, 2), technician=self.tech_b, work_type_id=_dir_id('worklog_type', 'repair'), description='Робота Б', hours_spent=2, ) self.client = Client() def test_filter_by_technician(self): login_2fa(self.client, self.tech_a) r = self.client.get(f'/it-manager/?tab=worklog&worklog_technician={self.tech_a.pk}') self.assertEqual(r.status_code, 200) self.assertContains(r, 'Робота А') self.assertNotContains(r, 'Робота Б') self.assertContains(r, 'name="worklog_technician"') self.assertContains(r, 'name="worklog_month"') self.assertContains(r, 'data-it-active-tab="worklog"') self.assertContains(r, 'id="it-panel-equipment"') self.assertRegex(r.content.decode(), r'id="it-panel-equipment"[^>]*style="display:none;"') self.assertRegex(r.content.decode(), r'id="it-panel-worklog"(?! style="display:none;")') def test_filter_by_month(self): WorkLog.objects.create( date=date(2026, 8, 15), technician=self.tech_a, work_type_id=_dir_id('worklog_type', 'maintenance'), description='Серпень робота', ) login_2fa(self.client, self.tech_a) r = self.client.get('/it-manager/?tab=worklog&worklog_month=2026-09') self.assertEqual(r.status_code, 200) self.assertContains(r, 'Робота А') self.assertContains(r, 'Робота Б') self.assertNotContains(r, 'Серпень робота') r_all = self.client.get('/it-manager/?tab=worklog&worklog_month=all') self.assertContains(r_all, 'Серпень робота') def test_filter_month_and_technician(self): login_2fa(self.client, self.tech_a) r = self.client.get( f'/it-manager/?tab=worklog&worklog_month=2026-09&worklog_technician={self.tech_b.pk}' ) self.assertEqual(r.status_code, 200) self.assertNotContains(r, 'Робота А') self.assertContains(r, 'Робота Б') def test_head_can_inline_update(self): login_2fa(self.client, self.head) r = self.client.post(f'/it-manager/worklog/{self.log_a.pk}/update/', { 'date': '2026-09-03', 'technician_id': self.tech_b.pk, 'work_type': 'software', 'description': 'Виправлено керівником', 'result': 'Ок', 'hours_spent': '3.5', 'location_id': '', }) self.assertEqual(r.status_code, 200) self.assertContains(r, 'Сохранено') self.log_a.refresh_from_db() self.assertEqual(self.log_a.description, 'Виправлено керівником') self.assertEqual(self.log_a.technician_id, self.tech_b.pk) self.assertEqual(float(self.log_a.hours_spent), 3.5) self.assertEqual(self.log_a.work_type.code, 'software') def test_non_head_cannot_inline_update(self): login_2fa(self.client, self.tech_a) r = self.client.post(f'/it-manager/worklog/{self.log_a.pk}/update/', { 'date': '2026-09-03', 'technician_id': self.tech_a.pk, 'work_type': 'maintenance', 'description': 'Спроба змінити', 'hours_spent': '9', }) self.assertEqual(r.status_code, 403) self.log_a.refresh_from_db() self.assertEqual(self.log_a.description, 'Робота А') def test_head_sees_editable_controls(self): login_2fa(self.client, self.head) r = self.client.get('/it-manager/?tab=worklog') self.assertEqual(r.status_code, 200) self.assertContains(r, f'worklog-form-{self.log_a.pk}') self.assertContains(r, 'править записи прямо в таблице')