"""Тести IT-порталу: доступ через групу IT, інвентаризація обладнання (зі списанням історії видачі), черга заявок, журнал робіт.""" from datetime import date import json import tempfile from pathlib import Path 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 apps.it_manager.work_directory import seed_from_json from core.models.directories import Directory from core.test_utils import login_2fa, grant_all_locations 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) MINI_WORK_CATALOG = { 'schema_version': 1, 'status': 'test', 'detail_fields': { 'device_type': {'label': 'Вид устройства'}, 'system': {'label': 'Система'}, }, 'categories': [ { 'id': 'helpdesk', 'name': 'Поддержка', 'works': [ {'id': 'helpdesk.test1', 'name': 'Приём обращений'}, ], }, { 'id': 'printing', 'name': 'Печать', 'works': [ { 'id': 'printing.test1', 'name': 'Восстановление печати', 'required_details': ['device_type'], }, ], }, ], } def seed_mini_work_catalog(): with tempfile.NamedTemporaryFile('w', suffix='.json', delete=False, encoding='utf-8') as fh: json.dump(MINI_WORK_CATALOG, fh, ensure_ascii=False) path = fh.name try: return seed_from_json(path) finally: Path(path).unlink(missing_ok=True) def catalog_work_id(code): return Directory.objects.get(directory_type__code='it_work', code=code).id 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')) grant_all_locations(self.it_user) 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, ) def test_search_by_serial_and_inventory(self): make_equipment(serial_number='SRCH-SN-AA', inventory_number='INV-1001') make_equipment(serial_number='OTHER-99', inventory_number='INV-2002') r = self.client.get('/it-manager/', {'tab': 'equipment', 'q': 'SRCH-SN'}) self.assertEqual(r.status_code, 200) serials = [e.serial_number for e in r.context['equipment_page']] self.assertEqual(serials, ['SRCH-SN-AA']) r = self.client.get('/it-manager/', {'tab': 'equipment', 'q': 'INV-2002'}) self.assertEqual(r.status_code, 200) serials = [e.serial_number for e in r.context['equipment_page']] self.assertEqual(serials, ['OTHER-99']) def test_filter_by_status_and_location(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-filt-reg', defaults={'name': 'Фильтр'}, ) loc_a = Location.objects.create( address='Лок А', location_type='pharmacy', location_region=region, is_active=True, ) loc_b = Location.objects.create( address='Лок Б', location_type='pharmacy', location_region=region, is_active=True, ) stock = make_equipment(serial_number='FILT-STOCK') assigned = make_equipment( serial_number='FILT-ASGN', status_id=_dir_id('equipment_status', 'assigned'), location=loc_a, ) make_equipment( serial_number='FILT-OTHER', status_id=_dir_id('equipment_status', 'assigned'), location=loc_b, ) r = self.client.get('/it-manager/', {'tab': 'equipment', 'status': 'stock'}) self.assertEqual(r.status_code, 200) serials = [e.serial_number for e in r.context['equipment_page']] self.assertIn('FILT-STOCK', serials) self.assertNotIn('FILT-ASGN', serials) r = self.client.get('/it-manager/', { 'tab': 'equipment', 'status': 'assigned', 'location_id': str(loc_a.pk), }) self.assertEqual(r.status_code, 200) serials = [e.serial_number for e in r.context['equipment_page']] self.assertEqual(serials, ['FILT-ASGN']) self.assertEqual(stock.status.code, 'stock') self.assertEqual(assigned.location_id, loc_a.pk) def test_update_inventory_number_and_notes(self): equipment = make_equipment(serial_number='INV-EDIT-1') r = self.client.post(f'/it-manager/equipment/{equipment.id}/update/', { 'status': 'stock', 'assign_target': '', 'inventory_number': 'INV-EDIT-99', 'notes': 'Проверено на складе', 'purchase_date': '2025-06-01', 'warranty_until': '2027-06-01', }, HTTP_HX_REQUEST='true') self.assertEqual(r.status_code, 200) equipment.refresh_from_db() self.assertEqual(equipment.inventory_number, 'INV-EDIT-99') self.assertEqual(equipment.notes, 'Проверено на складе') self.assertEqual(str(equipment.purchase_date), '2025-06-01') self.assertEqual(str(equipment.warranty_until), '2027-06-01') other = make_equipment(serial_number='INV-EDIT-2', inventory_number='INV-TAKEN') r = self.client.post(f'/it-manager/equipment/{equipment.id}/update/', { 'status': 'stock', 'assign_target': '', 'inventory_number': other.inventory_number, 'notes': equipment.notes, }, HTTP_HX_REQUEST='true') self.assertEqual(r.status_code, 200) self.assertContains(r, 'уже используется') equipment.refresh_from_db() self.assertEqual(equipment.inventory_number, 'INV-EDIT-99') def test_bulk_create_with_inventory_numbers(self): model = make_equipment_model() r = self.client.post('/it-manager/equipment/bulk-create/', { 'equipment_model_id': str(model.pk), 'serial_numbers': 'SN-INV-1\nSN-INV-2', 'inventory_numbers': 'INV-A1\nINV-A2', }, HTTP_HX_REQUEST='true') self.assertEqual(r.status_code, 200) u1 = Equipment.objects.get(serial_number='SN-INV-1') u2 = Equipment.objects.get(serial_number='SN-INV-2') self.assertEqual(u1.inventory_number, 'INV-A1') self.assertEqual(u2.inventory_number, 'INV-A2') r = self.client.post('/it-manager/equipment/bulk-create/', { 'equipment_model_id': str(model.pk), 'serial_numbers': 'SN-INV-3\nSN-INV-4', 'inventory_numbers': 'INV-ONLY-ONE', }, HTTP_HX_REQUEST='true') self.assertEqual(r.status_code, 200) self.assertContains(r, 'должно совпадать') self.assertFalse(Equipment.objects.filter(serial_number='SN-INV-3').exists()) def test_assignment_history_visible_in_card(self): equipment = make_equipment(serial_number='HIST-UI-1') emp = make_employee(first_name='Иван', last_name='Историков') self.client.post(f'/it-manager/equipment/{equipment.id}/update/', { 'status': 'assigned', 'assign_target': 'employee', 'assigned_to_id': emp.id, }) r = self.client.get('/it-manager/', {'tab': 'equipment', 'q': 'HIST-UI-1'}) self.assertEqual(r.status_code, 200) self.assertContains(r, 'История перемещений') self.assertContains(r, 'Историков') self.assertContains(r, 'открыта') class ItManagerTicketTests(TestCase): def setUp(self): self.it_user = make_employee(is_staff=False) self.it_user.groups.add(Group.objects.get(name='IT')) grant_all_locations(self.it_user) 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 ItWorkDirectorySeedTests(TestCase): def test_seed_upserts_categories_and_works(self): stats = seed_mini_work_catalog() self.assertEqual(stats['categories'], 2) self.assertEqual(stats['works'], 2) self.assertEqual( Directory.objects.filter(directory_type__code='it_work_category', is_active=True).count(), 2, ) work = Directory.objects.get(directory_type__code='it_work', code='printing.test1') self.assertEqual(work.extra_data.get('required_details'), ['device_type']) # idempotent stats2 = seed_mini_work_catalog() self.assertEqual(stats2['works'], 2) self.assertEqual( Directory.objects.filter(directory_type__code='it_work').exclude(code='_meta').count(), 2, ) class ItManagerWorkLogTests(TestCase): def setUp(self): seed_mini_work_catalog() self.it_user = make_employee(is_staff=False) self.it_user.groups.add(Group.objects.get(name='IT')) grant_all_locations(self.it_user) self.client = Client() login_2fa(self.client, self.it_user) def test_create_worklog(self): r = self.client.post('/it-manager/worklog/create/', { 'catalog_work_id': str(catalog_work_id('helpdesk.test1')), '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(log.catalog_work.code, 'helpdesk.test1') self.assertIsNone(log.work_type_id) self.assertEqual(float(log.hours_spent), 2.5) self.assertContains(r, 'Запись о выполненной работе сохранена') def test_create_worklog_requires_catalog_and_details(self): before = WorkLog.objects.count() r = self.client.post('/it-manager/worklog/create/', { 'description': 'без работы', }) self.assertEqual(r.status_code, 200) self.assertEqual(WorkLog.objects.count(), before) self.assertContains(r, 'типовую работу') r = self.client.post('/it-manager/worklog/create/', { 'catalog_work_id': str(catalog_work_id('printing.test1')), 'description': '', }) self.assertEqual(r.status_code, 200) self.assertEqual(WorkLog.objects.count(), before) self.assertContains(r, 'обязательные уточнения') r = self.client.post('/it-manager/worklog/create/', { 'catalog_work_id': str(catalog_work_id('printing.test1')), 'detail_device_type': 'МФУ', }) self.assertEqual(r.status_code, 200) self.assertEqual(WorkLog.objects.count(), before + 1) log = WorkLog.objects.latest('pk') self.assertEqual(log.details.get('device_type'), 'МФУ') def test_worklog_list_shows_catalog_title(self): WorkLog.objects.create( technician=self.it_user, catalog_work_id=catalog_work_id('printing.test1'), details={'device_type': 'Принтер'}, description='на складе', ) WorkLog.objects.create( technician=self.it_user, work_type_id=_dir_id('worklog_type', 'maintenance'), description='Старая запись без справочника', ) r = self.client.get('/it-manager/?tab=worklog') self.assertEqual(r.status_code, 200) self.assertContains(r, 'Восстановление печати') self.assertContains(r, 'без справочника') self.assertContains(r, 'Старая запись без справочника') def test_create_worklog_linked_to_ticket(self): from apps.it_manager.models import Ticket from core.models.directories import 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/', { 'catalog_work_id': str(catalog_work_id('helpdesk.test1')), '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) def test_monthly_builder_mix_catalog_and_legacy(self): from apps.reports.it_monthly.builder import build_monthly_report WorkLog.objects.create( date=date(2026, 5, 10), technician=self.it_user, catalog_work_id=catalog_work_id('printing.test1'), details={'device_type': 'МФУ'}, description='на А3', ) WorkLog.objects.create( date=date(2026, 5, 11), technician=self.it_user, work_type_id=_dir_id('worklog_type', 'repair'), description='Старый свободный текст', ) report = build_monthly_report(2026, 5, company={'staff': [], 'directions': []}) works = [ t['work'] for emp in report['employees'] for t in emp.get('tasks') or [] ] self.assertTrue(any('Восстановление печати' in w and 'МФУ' in w for w in works)) self.assertTrue(any('Старый свободный текст' in w for w in works)) 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) grant_all_locations(u) 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, 'править записи прямо в таблице')