Files
ACORE/apps/it_manager/tests.py
T

1029 lines
44 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Тести 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_equipment_create_is_popup(self):
r = self.client.get('/it-manager/')
self.assertContains(r, 'id="it-equipment-panel"')
self.assertContains(r, 'it-equipment-modal-title')
self.assertContains(r, 'role="dialog"')
self.assertNotContains(r, 'id="it-equipment-panel-overlay"')
def test_models_catalog_is_popup(self):
r = self.client.get('/it-manager/')
self.assertContains(r, 'id="it-equipment-models-panel"')
self.assertContains(r, 'it-equipment-models-modal-title')
self.assertContains(r, 'itModelsOpenFromEquipment')
self.assertNotContains(r, 'id="it-equipment-models-panel-overlay"')
def test_model_create_returns_to_equipment_popup(self):
r = self.client.post('/it-manager/equipment/models/create/', {
'equipment_type': 'laptop',
'name': 'Latitude 5440',
'return_to': 'equipment',
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
model = EquipmentModel.objects.get(name='Latitude 5440')
self.assertContains(r, 'data-it-equipment-open="1"')
self.assertContains(r, f'data-it-select-model-id="{model.pk}"')
self.assertNotContains(r, 'data-it-models-open="1"')
def test_model_create_error_reopens_models_popup(self):
r = self.client.post('/it-manager/equipment/models/create/', {
'equipment_type': 'laptop',
'name': '',
'return_to': 'equipment',
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'data-it-models-open="1"')
self.assertContains(r, 'name="return_to"')
self.assertContains(r, 'it-equipment-models-modal-title')
def test_bulk_create_error_reopens_popup(self):
r = self.client.post('/it-manager/equipment/bulk-create/', {
'serial_numbers': 'SN-X',
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'data-it-equipment-open="1"')
self.assertContains(r, 'it-equipment-modal-title')
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_equipment_labels_page(self):
from apps.it_manager.equipment_labels import equipment_ean13, equipment_qr_payload
eq = make_equipment(serial_number='LBL-SN-1', inventory_number='INV-LBL-1')
self.assertEqual(equipment_qr_payload(eq), 'INV-LBL-1')
ean = equipment_ean13(eq.pk)
self.assertEqual(len(ean), 13)
self.assertTrue(ean.startswith('200'))
r = self.client.get(f'/it-manager/equipment/labels/?ids={eq.pk}&codes=both')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'INV-LBL-1')
self.assertContains(r, 'data:image/png;base64,')
self.assertContains(r, ean)
# позиции в CSS с точкой (не запятой локали)
self.assertContains(r, 'left:')
self.assertRegex(r.content.decode('utf-8'), r'left:\d+\.\d+mm')
r_qr = self.client.get(f'/it-manager/equipment/labels/?ids={eq.pk}&codes=qr')
self.assertEqual(r_qr.status_code, 200)
self.assertContains(r_qr, 'INV-LBL-1')
self.assertNotContains(r_qr, f'alt="{ean}"')
r_ean = self.client.get(f'/it-manager/equipment/labels/?ids={eq.pk}&codes=ean')
self.assertEqual(r_ean.status_code, 200)
self.assertContains(r_ean, ean)
self.assertNotContains(r_ean, f'alt="INV-LBL-1"')
r2 = self.client.get('/it-manager/', {'tab': 'equipment', 'q': ean})
self.assertEqual(r2.status_code, 200)
serials = [e.serial_number for e in r2.context['equipment_page']]
self.assertIn('LBL-SN-1', serials)
def test_auto_inventory_on_bulk_create(self):
from apps.it_manager.equipment_labels import suggested_inventory_number
model = make_equipment_model()
r = self.client.post('/it-manager/equipment/bulk-create/', {
'equipment_model_id': str(model.pk),
'serial_numbers': 'SN-AUTO-INV-1',
'auto_inventory': '1',
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
eq = Equipment.objects.get(serial_number='SN-AUTO-INV-1')
self.assertEqual(eq.inventory_number, suggested_inventory_number(eq.pk))
def test_qr_custom_pattern(self):
from apps.it_manager.equipment_labels import equipment_qr_payload, label_payload_for_unit
from apps.it_manager.models import EquipmentLabelTemplate
eq = make_equipment(serial_number='QR-SN-9', inventory_number='INV-QR-9')
self.assertEqual(equipment_qr_payload(eq), 'INV-QR-9')
self.assertEqual(
equipment_qr_payload(eq, 'ACORE|{serial}|{inventory}'),
'ACORE|QR-SN-9|INV-QR-9',
)
self.assertEqual(equipment_qr_payload(eq, 'id={id}'), f'id={eq.pk}')
tmpl = EquipmentLabelTemplate.get_default()
tmpl.layout = [
{
'id': 'qr1', 'type': 'qr',
'x_mm': 2, 'y_mm': 2, 'w_mm': 22, 'h_mm': 22,
'font_pt': 9, 'bold': False, 'align': 'left',
'text': 'S:{serial}',
},
]
tmpl.width_mm = 40
tmpl.height_mm = 30
tmpl.save()
payload = label_payload_for_unit(eq, template=tmpl)
qr_blocks = [b for b in payload['blocks'] if b['type'] == 'qr']
self.assertEqual(qr_blocks[0]['text'], 'S:QR-SN-9')
def test_labels_inject_text_when_template_codes_only(self):
from apps.it_manager.equipment_labels import label_payload_for_unit
from apps.it_manager.models import EquipmentLabelTemplate
tmpl = EquipmentLabelTemplate.get_default()
tmpl.layout = [
{
'id': 'qr', 'type': 'qr',
'x_mm': 2, 'y_mm': 2, 'w_mm': 20, 'h_mm': 20,
'font_pt': 9, 'bold': False, 'align': 'left', 'text': '',
},
{
'id': 'ean', 'type': 'ean13',
'x_mm': 2, 'y_mm': 24, 'w_mm': 60, 'h_mm': 14,
'font_pt': 8, 'bold': False, 'align': 'center', 'text': '',
},
]
tmpl.save(update_fields=['layout'])
eq = make_equipment(serial_number='TXT-SN-1', inventory_number='INV-TXT-1')
payload = label_payload_for_unit(eq, template=tmpl)
types = {b['type'] for b in payload['blocks']}
self.assertIn('qr', types)
self.assertIn('ean13', types)
self.assertIn('model', types)
self.assertIn('inventory', types)
inv_block = next(b for b in payload['blocks'] if b['type'] == 'inventory')
self.assertEqual(inv_block['text'], 'INV-TXT-1')
r = self.client.get(f'/it-manager/equipment/labels/?ids={eq.pk}')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'INV-TXT-1')
self.assertContains(r, 'block text')
def test_label_template_editor(self):
import json
from apps.it_manager.models import EquipmentLabelTemplate
r = self.client.get('/it-manager/equipment/labels/template/')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'Шаблон этикетки')
# json_script должен отдать объект, а не JSON-строку (двойной dumps ломал редактор)
self.assertContains(r, '"type": "qr"')
self.assertNotContains(r, '\\"type\\": \\"qr\\"')
tmpl = EquipmentLabelTemplate.get_default()
payload = {
'width_mm': 80,
'height_mm': 50,
'layout': [
{
'id': 'qr1', 'type': 'qr',
'x_mm': 3, 'y_mm': 3, 'w_mm': 20, 'h_mm': 20,
'font_pt': 9, 'bold': False, 'align': 'left', 'text': '',
},
{
'id': 'm1', 'type': 'model',
'x_mm': 26, 'y_mm': 3, 'w_mm': 40, 'h_mm': 8,
'font_pt': 10, 'bold': True, 'align': 'left', 'text': '',
},
],
}
r_save = self.client.post(
'/it-manager/equipment/labels/template/',
data=json.dumps(payload),
content_type='application/json',
)
self.assertEqual(r_save.status_code, 200)
data = r_save.json()
self.assertTrue(data.get('ok'))
tmpl.refresh_from_db()
self.assertEqual(tmpl.width_mm, 80)
self.assertEqual(len(tmpl.layout), 2)
outsider = make_employee(username='no-it-labels')
client2 = Client()
login_2fa(client2, outsider)
forbidden = client2.get('/it-manager/equipment/labels/template/')
self.assertIn(forbidden.status_code, (403, 302, 200))
if forbidden.status_code == 200:
# deny_section_access может вернуть dashboard-фрагмент
self.assertNotContains(forbidden, 'label-canvas')
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_worklog_create_is_popup(self):
r = self.client.get('/it-manager/?tab=worklog')
self.assertContains(r, 'id="it-worklog-panel"')
self.assertContains(r, 'it-worklog-modal-title')
self.assertContains(r, 'role="dialog"')
self.assertNotContains(r, 'id="it-worklog-panel-overlay"')
def test_worklog_error_reopens_popup(self):
r = self.client.post('/it-manager/worklog/create/', {
'description': 'без работы',
}, HTTP_HX_REQUEST='true')
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'data-it-worklog-open="1"')
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_equipment_filter_hx_scoped_to_own_selects(self):
"""Фильтры оборудования не должны слушать чужие select (worklog и т.д.)."""
r = self.client.get('/it-manager/', {'tab': 'worklog'})
self.assertEqual(r.status_code, 200)
html = r.content.decode()
self.assertIn("change from:find select", html)
self.assertNotIn('change from:select,', html)
self.assertContains(r, 'data-it-active-tab="worklog"')
r2 = self.client.get('/it-manager/', {
'worklog_technician': str(self.it_user.pk),
})
self.assertEqual(r2.status_code, 200)
self.assertContains(r2, 'data-it-active-tab="worklog"')
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
seed_mini_work_catalog()
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, 'можно менять типовую работу')
self.assertContains(r, f'/it-manager/worklog/{self.log_a.pk}/delete/')
def test_head_can_change_catalog_work(self):
login_2fa(self.client, self.head)
log = WorkLog.objects.create(
date=date(2026, 9, 5),
technician=self.tech_a,
catalog_work_id=catalog_work_id('helpdesk.test1'),
description='бывший helpdesk',
)
new_id = catalog_work_id('printing.test1')
r = self.client.post(f'/it-manager/worklog/{log.pk}/update/', {
'date': '2026-09-05',
'technician_id': self.tech_a.pk,
'catalog_work_id': str(new_id),
'detail_device_type': 'Принтер',
'description': 'исправлено',
'hours_spent': '1',
'location_id': '',
})
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'Сохранено')
log.refresh_from_db()
self.assertEqual(log.catalog_work_id, new_id)
self.assertEqual(log.details.get('device_type'), 'Принтер')
self.assertIsNone(log.work_type_id)
def test_head_can_delete_worklog(self):
login_2fa(self.client, self.head)
pk = self.log_a.pk
r = self.client.post(f'/it-manager/worklog/{pk}/delete/')
self.assertEqual(r.status_code, 200)
self.assertFalse(WorkLog.objects.filter(pk=pk).exists())
def test_non_head_cannot_delete_worklog(self):
login_2fa(self.client, self.tech_a)
pk = self.log_a.pk
r = self.client.post(f'/it-manager/worklog/{pk}/delete/')
self.assertEqual(r.status_code, 403)
self.assertTrue(WorkLog.objects.filter(pk=pk).exists())