it_manager+reports: иерархия договоров, биллинг и месячные отчёты
Иерархия рамочных/дочерних договоров и платежей, интернет-биллинг, месячные IT-отчёты (импорт, DOCX/PDF, шаблоны выводов). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+279
-8
@@ -10,6 +10,7 @@ from apps.it_manager.models import (
|
||||
Equipment, EquipmentAssignment, EquipmentModel, Ticket, WorkLog,
|
||||
)
|
||||
from core.models.directories import Directory
|
||||
from core.test_utils import login_2fa
|
||||
|
||||
Employee = get_user_model()
|
||||
|
||||
@@ -67,7 +68,8 @@ class ItManagerAccessTests(TestCase):
|
||||
|
||||
def test_page_forbidden_for_non_it(self):
|
||||
r = self.client.get('/it-manager/')
|
||||
self.assertEqual(r.status_code, 403)
|
||||
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/', {
|
||||
@@ -88,7 +90,7 @@ class ItManagerEquipmentTests(TestCase):
|
||||
self.it_user = make_employee(is_staff=False)
|
||||
self.it_user.groups.add(Group.objects.get(name='IT'))
|
||||
self.client = Client()
|
||||
self.client.force_login(self.it_user)
|
||||
login_2fa(self.client, self.it_user)
|
||||
|
||||
def test_page_accessible_for_it_group(self):
|
||||
r = self.client.get('/it-manager/')
|
||||
@@ -119,6 +121,7 @@ class ItManagerEquipmentTests(TestCase):
|
||||
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)
|
||||
@@ -134,7 +137,7 @@ class ItManagerEquipmentTests(TestCase):
|
||||
emp2 = make_employee()
|
||||
|
||||
self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
|
||||
'status': 'assigned', 'assigned_to_id': emp1.id,
|
||||
'status': 'assigned', 'assign_target': 'employee', 'assigned_to_id': emp1.id,
|
||||
})
|
||||
equipment.refresh_from_db()
|
||||
self.assertEqual(equipment.assigned_to_id, emp1.id)
|
||||
@@ -142,7 +145,7 @@ class ItManagerEquipmentTests(TestCase):
|
||||
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', 'assigned_to_id': emp2.id,
|
||||
'status': 'assigned', 'assign_target': 'employee', 'assigned_to_id': emp2.id,
|
||||
})
|
||||
equipment.refresh_from_db()
|
||||
self.assertEqual(equipment.assigned_to_id, emp2.id)
|
||||
@@ -156,16 +159,115 @@ class ItManagerEquipmentTests(TestCase):
|
||||
equipment = make_equipment()
|
||||
emp1 = make_employee()
|
||||
self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
|
||||
'status': 'assigned', 'assigned_to_id': emp1.id,
|
||||
'status': 'assigned', 'assign_target': 'employee', 'assigned_to_id': emp1.id,
|
||||
})
|
||||
self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
|
||||
'status': 'stock', 'assigned_to_id': '',
|
||||
'status': 'stock', 'assign_target': '',
|
||||
})
|
||||
equipment.refresh_from_db()
|
||||
self.assertIsNone(equipment.assigned_to_id)
|
||||
self.assertEqual(equipment.status.code, 'stock')
|
||||
self.assertEqual(EquipmentAssignment.objects.filter(equipment=equipment, returned_at__isnull=True).count(), 0)
|
||||
|
||||
def test_bulk_assign_to_location_only(self):
|
||||
from apps.locations.models.base import Location
|
||||
from core.models.directories import DirectoryType
|
||||
|
||||
region_type, _ = DirectoryType.objects.get_or_create(
|
||||
code='location_region', defaults={'name': 'Регион'},
|
||||
)
|
||||
region, _ = Directory.objects.get_or_create(
|
||||
directory_type=region_type, code='eq-reg', defaults={'name': 'Тест'},
|
||||
)
|
||||
location = Location.objects.create(
|
||||
address='Аптека тест', location_type='pharmacy',
|
||||
location_region=region, is_active=True,
|
||||
)
|
||||
model = make_equipment_model(name='RB952Ui')
|
||||
unit = Equipment.objects.create(equipment_model=model, serial_number='RT-LOC-1')
|
||||
r = self.client.post('/it-manager/equipment/bulk-assign/', {
|
||||
'equipment_ids': [str(unit.pk)],
|
||||
'assign_target': 'pharmacy',
|
||||
'location_id': str(location.pk),
|
||||
}, HTTP_HX_REQUEST='true')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
unit.refresh_from_db()
|
||||
self.assertEqual(unit.location_id, location.pk)
|
||||
self.assertIsNone(unit.assigned_to_id)
|
||||
self.assertEqual(unit.status.code, 'assigned')
|
||||
hist = EquipmentAssignment.objects.get(equipment=unit, returned_at__isnull=True)
|
||||
self.assertEqual(hist.location_id, location.pk)
|
||||
self.assertIsNone(hist.assigned_to_id)
|
||||
|
||||
def test_update_assign_to_pharmacy(self):
|
||||
from apps.locations.models.base import Location
|
||||
from core.models.directories import DirectoryType
|
||||
|
||||
region_type, _ = DirectoryType.objects.get_or_create(
|
||||
code='location_region', defaults={'name': 'Регион'},
|
||||
)
|
||||
region, _ = Directory.objects.get_or_create(
|
||||
directory_type=region_type, code='eq-reg2', defaults={'name': 'Тест2'},
|
||||
)
|
||||
location = Location.objects.create(
|
||||
address='А12', location_type='pharmacy',
|
||||
location_region=region, is_active=True,
|
||||
)
|
||||
equipment = make_equipment(serial_number='RT-UPD-1')
|
||||
self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
|
||||
'status': 'assigned',
|
||||
'assign_target': 'pharmacy',
|
||||
'location_id': str(location.pk),
|
||||
})
|
||||
equipment.refresh_from_db()
|
||||
self.assertEqual(equipment.location_id, location.pk)
|
||||
self.assertIsNone(equipment.assigned_to_id)
|
||||
self.assertEqual(equipment.status.code, 'assigned')
|
||||
self.assertEqual(
|
||||
EquipmentAssignment.objects.filter(equipment=equipment, returned_at__isnull=True).count(), 1,
|
||||
)
|
||||
|
||||
# повернення на склад
|
||||
self.client.post(f'/it-manager/equipment/{equipment.id}/update/', {
|
||||
'status': 'stock',
|
||||
'assign_target': '',
|
||||
})
|
||||
equipment.refresh_from_db()
|
||||
self.assertIsNone(equipment.location_id)
|
||||
self.assertEqual(equipment.status.code, 'stock')
|
||||
self.assertEqual(
|
||||
EquipmentAssignment.objects.filter(equipment=equipment, returned_at__isnull=True).count(), 0,
|
||||
)
|
||||
|
||||
def test_bulk_create_with_location_writes_history(self):
|
||||
from apps.locations.models.base import Location
|
||||
from core.models.directories import DirectoryType
|
||||
|
||||
region_type, _ = DirectoryType.objects.get_or_create(
|
||||
code='location_region', defaults={'name': 'Регион'},
|
||||
)
|
||||
region, _ = Directory.objects.get_or_create(
|
||||
directory_type=region_type, code='eq-reg3', defaults={'name': 'Тест3'},
|
||||
)
|
||||
location = Location.objects.create(
|
||||
address='Склад-офіс', location_type='office',
|
||||
location_region=region, is_active=True,
|
||||
)
|
||||
model = make_equipment_model()
|
||||
r = self.client.post('/it-manager/equipment/bulk-create/', {
|
||||
'equipment_model_id': str(model.pk),
|
||||
'serial_numbers': 'SN-LOC-A',
|
||||
'assign_target': 'office',
|
||||
'location_id': str(location.pk),
|
||||
}, HTTP_HX_REQUEST='true')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
unit = Equipment.objects.get(serial_number='SN-LOC-A')
|
||||
self.assertEqual(unit.location_id, location.pk)
|
||||
self.assertEqual(unit.status.code, 'assigned')
|
||||
self.assertEqual(
|
||||
EquipmentAssignment.objects.filter(equipment=unit, location=location).count(), 1,
|
||||
)
|
||||
|
||||
|
||||
class ItManagerTicketTests(TestCase):
|
||||
|
||||
@@ -174,7 +276,7 @@ class ItManagerTicketTests(TestCase):
|
||||
self.it_user.groups.add(Group.objects.get(name='IT'))
|
||||
self.requester = make_employee()
|
||||
self.client = Client()
|
||||
self.client.force_login(self.it_user)
|
||||
login_2fa(self.client, self.it_user)
|
||||
|
||||
def test_resolve_ticket_stamps_resolved_at(self):
|
||||
ticket = Ticket.objects.create(
|
||||
@@ -210,7 +312,7 @@ class ItManagerWorkLogTests(TestCase):
|
||||
self.it_user = make_employee(is_staff=False)
|
||||
self.it_user.groups.add(Group.objects.get(name='IT'))
|
||||
self.client = Client()
|
||||
self.client.force_login(self.it_user)
|
||||
login_2fa(self.client, self.it_user)
|
||||
|
||||
def test_create_worklog(self):
|
||||
r = self.client.post('/it-manager/worklog/create/', {
|
||||
@@ -221,3 +323,172 @@ class ItManagerWorkLogTests(TestCase):
|
||||
log = WorkLog.objects.get(description='Профілактика серверної')
|
||||
self.assertEqual(log.technician_id, self.it_user.id)
|
||||
self.assertEqual(float(log.hours_spent), 2.5)
|
||||
self.assertContains(r, 'Запись о выполненной работе сохранена')
|
||||
|
||||
def test_create_worklog_requires_description(self):
|
||||
before = WorkLog.objects.count()
|
||||
r = self.client.post('/it-manager/worklog/create/', {
|
||||
'work_type': 'maintenance', 'description': ' ',
|
||||
})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(WorkLog.objects.count(), before)
|
||||
self.assertContains(r, 'Укажите описание работ')
|
||||
|
||||
def test_create_worklog_linked_to_ticket(self):
|
||||
from apps.it_manager.models import Ticket
|
||||
from core.models.directories import Directory, DirectoryType
|
||||
|
||||
ttype, _ = DirectoryType.objects.get_or_create(code='ticket_type', defaults={'name': 'Тип заявки'})
|
||||
ptype, _ = DirectoryType.objects.get_or_create(code='ticket_priority', defaults={'name': 'Приоритет'})
|
||||
stype, _ = DirectoryType.objects.get_or_create(code='ticket_status', defaults={'name': 'Статус'})
|
||||
ticket_type, _ = Directory.objects.get_or_create(
|
||||
directory_type=ttype, code='incident', defaults={'name': 'Инцидент'},
|
||||
)
|
||||
priority, _ = Directory.objects.get_or_create(
|
||||
directory_type=ptype, code='medium', defaults={'name': 'Средний'},
|
||||
)
|
||||
status, _ = Directory.objects.get_or_create(
|
||||
directory_type=stype, code='in_progress', defaults={'name': 'В работе'},
|
||||
)
|
||||
ticket = Ticket.objects.create(
|
||||
title='Не працює каса',
|
||||
ticket_type=ticket_type,
|
||||
priority=priority,
|
||||
status=status,
|
||||
requester=self.it_user,
|
||||
description='Каса не відкривається',
|
||||
)
|
||||
r = self.client.post('/it-manager/worklog/create/', {
|
||||
'work_type': 'repair',
|
||||
'description': 'Перезапуск касового ПЗ',
|
||||
'ticket_id': ticket.pk,
|
||||
'hours_spent': '1',
|
||||
})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
log = WorkLog.objects.get(description='Перезапуск касового ПЗ')
|
||||
self.assertEqual(log.ticket_id, ticket.pk)
|
||||
self.assertContains(r, 'Работа привязана к заявке')
|
||||
self.assertContains(r, ticket.number)
|
||||
|
||||
|
||||
class ItManagerWorkLogFilterEditTests(TestCase):
|
||||
"""Фильтр по сотруднику и inline-редактирование для руководителя."""
|
||||
|
||||
def setUp(self):
|
||||
from core.models.directories import DirectoryType
|
||||
|
||||
dept_type, _ = DirectoryType.objects.get_or_create(
|
||||
code='department', defaults={'name': 'Відділи', 'hierarchy_type': 'strict'},
|
||||
)
|
||||
self.dept = Directory.objects.create(
|
||||
directory_type=dept_type, code='it-wl', name='IT-відділ-wl',
|
||||
)
|
||||
self.head = make_employee(
|
||||
last_name='Керівник', first_name='ІТ', username='it.head.wl',
|
||||
department=self.dept, user_location_type='office',
|
||||
)
|
||||
self.tech_a = make_employee(
|
||||
last_name='Альфа', first_name='Тех', username='tech.a.wl',
|
||||
department=self.dept, manager=self.head, user_location_type='office',
|
||||
)
|
||||
self.tech_b = make_employee(
|
||||
last_name='Бета', first_name='Тех', username='tech.b.wl',
|
||||
department=self.dept, manager=self.head, user_location_type='office',
|
||||
)
|
||||
it_group = Group.objects.get(name='IT')
|
||||
for u in (self.head, self.tech_a, self.tech_b):
|
||||
u.groups.add(it_group)
|
||||
|
||||
self.log_a = WorkLog.objects.create(
|
||||
date=date(2026, 9, 1),
|
||||
technician=self.tech_a,
|
||||
work_type_id=_dir_id('worklog_type', 'maintenance'),
|
||||
description='Робота А',
|
||||
hours_spent=1,
|
||||
)
|
||||
self.log_b = WorkLog.objects.create(
|
||||
date=date(2026, 9, 2),
|
||||
technician=self.tech_b,
|
||||
work_type_id=_dir_id('worklog_type', 'repair'),
|
||||
description='Робота Б',
|
||||
hours_spent=2,
|
||||
)
|
||||
self.client = Client()
|
||||
|
||||
def test_filter_by_technician(self):
|
||||
login_2fa(self.client, self.tech_a)
|
||||
r = self.client.get(f'/it-manager/?tab=worklog&worklog_technician={self.tech_a.pk}')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, 'Робота А')
|
||||
self.assertNotContains(r, 'Робота Б')
|
||||
self.assertContains(r, 'name="worklog_technician"')
|
||||
self.assertContains(r, 'name="worklog_month"')
|
||||
self.assertContains(r, 'data-it-active-tab="worklog"')
|
||||
self.assertContains(r, 'id="it-panel-equipment"')
|
||||
self.assertRegex(r.content.decode(), r'id="it-panel-equipment"[^>]*style="display:none;"')
|
||||
self.assertRegex(r.content.decode(), r'id="it-panel-worklog"(?! style="display:none;")')
|
||||
|
||||
def test_filter_by_month(self):
|
||||
WorkLog.objects.create(
|
||||
date=date(2026, 8, 15),
|
||||
technician=self.tech_a,
|
||||
work_type_id=_dir_id('worklog_type', 'maintenance'),
|
||||
description='Серпень робота',
|
||||
)
|
||||
login_2fa(self.client, self.tech_a)
|
||||
r = self.client.get('/it-manager/?tab=worklog&worklog_month=2026-09')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, 'Робота А')
|
||||
self.assertContains(r, 'Робота Б')
|
||||
self.assertNotContains(r, 'Серпень робота')
|
||||
|
||||
r_all = self.client.get('/it-manager/?tab=worklog&worklog_month=all')
|
||||
self.assertContains(r_all, 'Серпень робота')
|
||||
|
||||
def test_filter_month_and_technician(self):
|
||||
login_2fa(self.client, self.tech_a)
|
||||
r = self.client.get(
|
||||
f'/it-manager/?tab=worklog&worklog_month=2026-09&worklog_technician={self.tech_b.pk}'
|
||||
)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertNotContains(r, 'Робота А')
|
||||
self.assertContains(r, 'Робота Б')
|
||||
|
||||
def test_head_can_inline_update(self):
|
||||
login_2fa(self.client, self.head)
|
||||
r = self.client.post(f'/it-manager/worklog/{self.log_a.pk}/update/', {
|
||||
'date': '2026-09-03',
|
||||
'technician_id': self.tech_b.pk,
|
||||
'work_type': 'software',
|
||||
'description': 'Виправлено керівником',
|
||||
'result': 'Ок',
|
||||
'hours_spent': '3.5',
|
||||
'location_id': '',
|
||||
})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, 'Сохранено')
|
||||
self.log_a.refresh_from_db()
|
||||
self.assertEqual(self.log_a.description, 'Виправлено керівником')
|
||||
self.assertEqual(self.log_a.technician_id, self.tech_b.pk)
|
||||
self.assertEqual(float(self.log_a.hours_spent), 3.5)
|
||||
self.assertEqual(self.log_a.work_type.code, 'software')
|
||||
|
||||
def test_non_head_cannot_inline_update(self):
|
||||
login_2fa(self.client, self.tech_a)
|
||||
r = self.client.post(f'/it-manager/worklog/{self.log_a.pk}/update/', {
|
||||
'date': '2026-09-03',
|
||||
'technician_id': self.tech_a.pk,
|
||||
'work_type': 'maintenance',
|
||||
'description': 'Спроба змінити',
|
||||
'hours_spent': '9',
|
||||
})
|
||||
self.assertEqual(r.status_code, 403)
|
||||
self.log_a.refresh_from_db()
|
||||
self.assertEqual(self.log_a.description, 'Робота А')
|
||||
|
||||
def test_head_sees_editable_controls(self):
|
||||
login_2fa(self.client, self.head)
|
||||
r = self.client.get('/it-manager/?tab=worklog')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, f'worklog-form-{self.log_a.pk}')
|
||||
self.assertContains(r, 'править записи прямо в таблице')
|
||||
|
||||
Reference in New Issue
Block a user