it_manager: справочник типовых работ в журнале WorkLog.
Категория/работа/required_details вместо свободного work_type; seed Directory, отчёт и тесты. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+139
-7
@@ -1,6 +1,9 @@
|
||||
"""Тести 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
|
||||
@@ -9,6 +12,7 @@ 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
|
||||
|
||||
@@ -58,6 +62,50 @@ def make_equipment(**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 (або суперюзеру)."""
|
||||
|
||||
@@ -432,9 +480,30 @@ class ItManagerTicketTests(TestCase):
|
||||
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)
|
||||
@@ -443,27 +512,65 @@ class ItManagerWorkLogTests(TestCase):
|
||||
|
||||
def test_create_worklog(self):
|
||||
r = self.client.post('/it-manager/worklog/create/', {
|
||||
'work_type': 'maintenance', 'description': 'Профілактика серверної',
|
||||
'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='Профілактика серверної')
|
||||
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_description(self):
|
||||
def test_create_worklog_requires_catalog_and_details(self):
|
||||
before = WorkLog.objects.count()
|
||||
r = self.client.post('/it-manager/worklog/create/', {
|
||||
'work_type': 'maintenance', 'description': ' ',
|
||||
'description': 'без работы',
|
||||
})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(WorkLog.objects.count(), before)
|
||||
self.assertContains(r, 'Укажите описание работ')
|
||||
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 Directory, DirectoryType
|
||||
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': 'Приоритет'})
|
||||
@@ -486,7 +593,7 @@ class ItManagerWorkLogTests(TestCase):
|
||||
description='Каса не відкривається',
|
||||
)
|
||||
r = self.client.post('/it-manager/worklog/create/', {
|
||||
'work_type': 'repair',
|
||||
'catalog_work_id': str(catalog_work_id('helpdesk.test1')),
|
||||
'description': 'Перезапуск касового ПЗ',
|
||||
'ticket_id': ticket.pk,
|
||||
'hours_spent': '1',
|
||||
@@ -497,6 +604,31 @@ class ItManagerWorkLogTests(TestCase):
|
||||
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-редактирование для руководителя."""
|
||||
|
||||
Reference in New Issue
Block a user