31bd4c8fa7
Категория/работа/required_details вместо свободного work_type; seed Directory, отчёт и тесты. Co-authored-by: Cursor <cursoragent@cursor.com>
755 lines
32 KiB
Python
755 lines
32 KiB
Python
"""Тести 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, 'править записи прямо в таблице')
|