Fix quality gate test failures
This commit is contained in:
parent
bf0df3613d
commit
2a598de8d5
@ -136,7 +136,11 @@ class MoySkladClient:
|
||||
"MoySklad GET transient failure %s; retrying in %.1f seconds",
|
||||
type(exc).__name__,
|
||||
retry_delay,
|
||||
extra={**log_context, "retry_in_seconds": retry_delay},
|
||||
extra={
|
||||
**log_context,
|
||||
"event": "commerce.moysklad.get_retry",
|
||||
"retry_in_seconds": retry_delay,
|
||||
},
|
||||
)
|
||||
time.sleep(retry_delay)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
@ -149,6 +153,7 @@ class MoySkladClient:
|
||||
"MoySklad HTTP %s; retrying GET",
|
||||
exc.response.status_code,
|
||||
extra={
|
||||
"event": "commerce.moysklad.get_retry",
|
||||
"endpoint": endpoint,
|
||||
"attempt": attempt,
|
||||
"request_offset": (params or {}).get("offset"),
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from communications.models import Chat, ChatMembership, Message
|
||||
from communications.models import Chat, ChatMembership
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
@ -19,7 +19,7 @@ pytestmark = pytest.mark.django_db
|
||||
|
||||
class TestChangeRoleImproved:
|
||||
"""Улучшенные тесты изменения ролей с проверкой через GET запрос"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def owner(self, db):
|
||||
return User.objects.create_user(
|
||||
@ -28,9 +28,10 @@ class TestChangeRoleImproved:
|
||||
first_name="Owner",
|
||||
last_name="Test",
|
||||
phone_number="+79001234567",
|
||||
is_active=True,
|
||||
send_activation_email=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def member_user(self, db):
|
||||
return User.objects.create_user(
|
||||
@ -39,94 +40,84 @@ class TestChangeRoleImproved:
|
||||
first_name="Member",
|
||||
last_name="Test",
|
||||
phone_number="+79001234568",
|
||||
is_active=True,
|
||||
send_activation_email=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_chat(self, owner, member_user):
|
||||
chat = Chat.objects.create(
|
||||
type="group",
|
||||
name="Test Chat",
|
||||
created_by=owner
|
||||
)
|
||||
chat = Chat.objects.create(type="group", name="Test Chat", created_by=owner)
|
||||
chat.participants.add(owner, member_user)
|
||||
|
||||
|
||||
ChatMembership.objects.create(
|
||||
chat=chat,
|
||||
user=member_user,
|
||||
role='member',
|
||||
invited_by=owner
|
||||
chat=chat, user=member_user, role="member", invited_by=owner
|
||||
)
|
||||
|
||||
|
||||
return chat
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def owner_client(self, owner):
|
||||
client = APIClient()
|
||||
client.force_authenticate(user=owner)
|
||||
return client
|
||||
|
||||
|
||||
def test_change_role_and_verify_via_api(self, owner_client, test_chat, member_user):
|
||||
"""
|
||||
Тест: изменить роль, дождаться ответа, проверить через GET
|
||||
"""
|
||||
# Шаг 1: Изменяем роль через POST
|
||||
change_url = f"/api/v1/communications/chats/{test_chat.id}/change-role/"
|
||||
response = owner_client.post(change_url, {
|
||||
'user_id': member_user.id,
|
||||
'role': 'admin'
|
||||
})
|
||||
|
||||
response = owner_client.post(
|
||||
change_url, {"user_id": member_user.id, "role": "admin"}
|
||||
)
|
||||
|
||||
# Шаг 2: Проверяем успешный ответ
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data['ok'] is True
|
||||
assert data['membership']['role'] == 'admin'
|
||||
|
||||
assert data["ok"] is True
|
||||
assert data["membership"]["role"] == "admin"
|
||||
|
||||
# Шаг 3: Получаем чат через GET и проверяем роль
|
||||
chat_url = f"/api/v1/communications/chats/{test_chat.id}/"
|
||||
get_response = owner_client.get(chat_url)
|
||||
|
||||
|
||||
assert get_response.status_code == status.HTTP_200_OK
|
||||
chat_data = get_response.json()
|
||||
|
||||
|
||||
# Шаг 4: Находим участника и проверяем роль
|
||||
member_membership = next(
|
||||
(m for m in chat_data['memberships'] if m['user'] == member_user.id),
|
||||
None
|
||||
(m for m in chat_data["memberships"] if m["user"] == member_user.id), None
|
||||
)
|
||||
|
||||
|
||||
assert member_membership is not None, "Участник не найден"
|
||||
assert member_membership['role'] == 'admin'
|
||||
assert member_membership['can_send_messages'] is True
|
||||
assert member_membership['can_add_members'] is True
|
||||
|
||||
assert member_membership["role"] == "admin"
|
||||
assert member_membership["can_send_messages"] is True
|
||||
assert member_membership["can_add_members"] is True
|
||||
|
||||
def test_member_to_guest_role_change(self, owner_client, test_chat, member_user):
|
||||
"""
|
||||
Тест: изменение member → guest с проверкой через API
|
||||
"""
|
||||
# Шаг 1: Изменяем роль
|
||||
change_url = f"/api/v1/communications/chats/{test_chat.id}/change-role/"
|
||||
response = owner_client.post(change_url, {
|
||||
'user_id': member_user.id,
|
||||
'role': 'guest'
|
||||
})
|
||||
|
||||
response = owner_client.post(
|
||||
change_url, {"user_id": member_user.id, "role": "guest"}
|
||||
)
|
||||
|
||||
# Шаг 2: Проверяем ответ
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
|
||||
# Шаг 3: Проверяем через GET
|
||||
chat_url = f"/api/v1/communications/chats/{test_chat.id}/"
|
||||
get_response = owner_client.get(chat_url)
|
||||
|
||||
|
||||
chat_data = get_response.json()
|
||||
member_membership = next(
|
||||
(m for m in chat_data['memberships'] if m['user'] == member_user.id),
|
||||
None
|
||||
(m for m in chat_data["memberships"] if m["user"] == member_user.id), None
|
||||
)
|
||||
|
||||
|
||||
# Шаг 4: Проверяем что стал гостем (только чтение)
|
||||
assert member_membership['role'] == 'guest'
|
||||
assert member_membership['can_send_messages'] is False
|
||||
assert member_membership['can_add_members'] is False
|
||||
assert member_membership['can_remove_members'] is False
|
||||
assert member_membership["role"] == "guest"
|
||||
assert member_membership["can_send_messages"] is False
|
||||
assert member_membership["can_add_members"] is False
|
||||
assert member_membership["can_remove_members"] is False
|
||||
|
||||
@ -206,9 +206,7 @@ def test_department_calendar_event_permissions_for_non_member_and_member(
|
||||
)
|
||||
assert forbidden_response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
forbidden_delete = api_client.delete(
|
||||
f"/api/v1/schedule/events/{member_event.id}/"
|
||||
)
|
||||
forbidden_delete = api_client.delete(f"/api/v1/schedule/events/{member_event.id}/")
|
||||
assert forbidden_delete.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@ -293,9 +291,7 @@ def test_department_calendar_participants_include_avatar_and_email(
|
||||
"calendar_id"
|
||||
]
|
||||
|
||||
response = api_client.get(
|
||||
f"/api/v1/schedule/calendars/{calendar_id}/participants/"
|
||||
)
|
||||
response = api_client.get(f"/api/v1/schedule/calendars/{calendar_id}/participants/")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.data) == 1
|
||||
@ -307,7 +303,7 @@ def test_department_calendar_participants_include_avatar_and_email(
|
||||
assert participant["user"]["last_name"] == "User"
|
||||
|
||||
|
||||
def test_department_calendar_occurrences_allow_outsider_but_keep_permissions(
|
||||
def test_department_calendar_occurrences_hide_events_from_outsider(
|
||||
api_client: APIClient,
|
||||
):
|
||||
head = make_user("head@example.com")
|
||||
@ -344,9 +340,19 @@ def test_department_calendar_occurrences_allow_outsider_but_keep_permissions(
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == []
|
||||
|
||||
api_client.force_authenticate(user=member)
|
||||
response = api_client.get(
|
||||
"/api/v1/schedule/events/occurrences/",
|
||||
{
|
||||
"calendar": calendar_id,
|
||||
"start": (start - timedelta(minutes=5)).isoformat(),
|
||||
"end": (start + timedelta(hours=2)).isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.data) == 1
|
||||
occurrence = response.data[0]
|
||||
assert occurrence["event_id"] == member_event.id
|
||||
assert occurrence["can_edit"] is False
|
||||
assert occurrence["can_delete"] is False
|
||||
assert response.data[0]["event_id"] == member_event.id
|
||||
|
||||
@ -28,10 +28,7 @@ def api_client():
|
||||
@pytest.fixture
|
||||
def department(db):
|
||||
"""Тестовый отдел."""
|
||||
return Department.objects.create(
|
||||
name="IT отдел",
|
||||
description="Отдел IT"
|
||||
)
|
||||
return Department.objects.create(name="IT отдел", description="Отдел IT")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -46,9 +43,7 @@ def user(db, department):
|
||||
send_activation_email=False,
|
||||
)
|
||||
EmployeeDepartment.objects.create(
|
||||
employee=user,
|
||||
department=department,
|
||||
is_active=True
|
||||
employee=user, department=department, is_active=True
|
||||
)
|
||||
return user
|
||||
|
||||
@ -71,9 +66,7 @@ def staff_user(db):
|
||||
def category(db):
|
||||
"""Категория оборудования."""
|
||||
return EquipmentCategory.objects.create(
|
||||
name="Ноутбуки",
|
||||
description="Портативные компьютеры",
|
||||
icon="bi-laptop"
|
||||
name="Ноутбуки", description="Портативные компьютеры", icon="bi-laptop"
|
||||
)
|
||||
|
||||
|
||||
@ -106,33 +99,29 @@ class TestEquipmentList:
|
||||
|
||||
def test_list_unauthorized(self, api_client):
|
||||
"""Неавторизованный доступ запрещен."""
|
||||
url = reverse('api:v1:procurement:equipment-list')
|
||||
url = reverse("api:v1:procurement:equipment-list")
|
||||
response = api_client.get(url)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_list_equipment(
|
||||
self, api_client, user, equipment
|
||||
):
|
||||
def test_list_equipment(self, api_client, user, equipment):
|
||||
"""Получение списка оборудования."""
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse('api:v1:procurement:equipment-list')
|
||||
url = reverse("api:v1:procurement:equipment-list")
|
||||
response = api_client.get(url)
|
||||
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
results = response.data['results']
|
||||
results = response.data["results"]
|
||||
assert len(results) >= 1
|
||||
|
||||
|
||||
# Проверяем структуру
|
||||
item = results[0]
|
||||
assert 'inventory_number' in item
|
||||
assert 'name' in item
|
||||
assert 'status' in item
|
||||
assert 'category_name' in item
|
||||
assert 'department_name' in item
|
||||
assert "inventory_number" in item
|
||||
assert "name" in item
|
||||
assert "status" in item
|
||||
assert "category_name" in item
|
||||
assert "department_name" in item
|
||||
|
||||
def test_filter_by_status(
|
||||
self, api_client, user, category, department, equipment
|
||||
):
|
||||
def test_filter_by_status(self, api_client, user, category, department, equipment):
|
||||
"""Фильтр по статусу."""
|
||||
# Создаем оборудование с разными статусами
|
||||
Equipment.objects.create(
|
||||
@ -144,23 +133,22 @@ class TestEquipmentList:
|
||||
purchase_date=date.today(),
|
||||
purchase_cost=Decimal("20000.00"),
|
||||
)
|
||||
|
||||
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse('api:v1:procurement:equipment-list')
|
||||
|
||||
url = reverse("api:v1:procurement:equipment-list")
|
||||
|
||||
# Фильтр AVAILABLE
|
||||
response = api_client.get(url, {'status': EquipmentStatus.AVAILABLE})
|
||||
response = api_client.get(url, {"status": EquipmentStatus.AVAILABLE})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
results = response.data['results']
|
||||
assert all(r['status'] == EquipmentStatus.AVAILABLE for r in results)
|
||||
results = response.data["results"]
|
||||
assert all(r["status"] == EquipmentStatus.AVAILABLE for r in results)
|
||||
|
||||
def test_filter_by_department(
|
||||
self, api_client, user, db, category, department, equipment
|
||||
):
|
||||
"""Фильтр по отделу."""
|
||||
other_dept = Department.objects.create(
|
||||
name="HR отдел",
|
||||
description="Другой отдел"
|
||||
name="HR отдел", description="Другой отдел"
|
||||
)
|
||||
Equipment.objects.create(
|
||||
name="Принтер",
|
||||
@ -171,26 +159,24 @@ class TestEquipmentList:
|
||||
purchase_date=date.today(),
|
||||
purchase_cost=Decimal("15000.00"),
|
||||
)
|
||||
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse('api:v1:procurement:equipment-list')
|
||||
|
||||
response = api_client.get(url, {'department': department.id})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
results = response.data['results']
|
||||
# Все должны быть из нужного отдела
|
||||
assert all(r['department'] == department.id for r in results)
|
||||
|
||||
def test_search_equipment(
|
||||
self, api_client, user, equipment
|
||||
):
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse("api:v1:procurement:equipment-list")
|
||||
|
||||
response = api_client.get(url, {"department": department.id})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
results = response.data["results"]
|
||||
# Все должны быть из нужного отдела
|
||||
assert all(r["department"] == department.id for r in results)
|
||||
|
||||
def test_search_equipment(self, api_client, user, equipment):
|
||||
"""Поиск оборудования."""
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse('api:v1:procurement:equipment-list')
|
||||
|
||||
response = api_client.get(url, {'search': 'latitude'})
|
||||
url = reverse("api:v1:procurement:equipment-list")
|
||||
|
||||
response = api_client.get(url, {"search": "latitude"})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.data['results']) >= 1
|
||||
assert len(response.data["results"]) >= 1
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
@ -206,44 +192,44 @@ class TestEquipmentCreate:
|
||||
):
|
||||
"""Создание оборудования staff пользователем."""
|
||||
api_client.force_authenticate(user=staff_user)
|
||||
url = reverse('api:v1:procurement:equipment-list')
|
||||
|
||||
url = reverse("api:v1:procurement:equipment-list")
|
||||
|
||||
data = {
|
||||
'name': 'Монитор LG 27"',
|
||||
'inventory_number': 'INV-2026-100',
|
||||
'serial_number': 'LG987654321',
|
||||
'category': category.id,
|
||||
'status': EquipmentStatus.AVAILABLE,
|
||||
'department': department.id,
|
||||
'location': 'Склад',
|
||||
'purchase_date': '2026-02-01',
|
||||
'warranty_until': '2029-02-01',
|
||||
'purchase_cost': '25000.00',
|
||||
'notes': 'Для офиса',
|
||||
"name": 'Монитор LG 27"',
|
||||
"inventory_number": "INV-2026-100",
|
||||
"serial_number": "LG987654321",
|
||||
"category": category.id,
|
||||
"status": EquipmentStatus.AVAILABLE,
|
||||
"department": department.id,
|
||||
"location": "Склад",
|
||||
"purchase_date": "2026-02-01",
|
||||
"warranty_until": "2029-02-01",
|
||||
"purchase_cost": "25000.00",
|
||||
"notes": "Для офиса",
|
||||
}
|
||||
|
||||
response = api_client.post(url, data, format='json')
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data['name'] == 'Монитор LG 27"'
|
||||
assert response.data['inventory_number'].startswith('INV-')
|
||||
assert response.data["name"] == 'Монитор LG 27"'
|
||||
assert response.data["inventory_number"].startswith("INV-")
|
||||
|
||||
def test_create_equipment_regular_user_forbidden(
|
||||
self, api_client, user, category, department
|
||||
):
|
||||
"""Обычный пользователь не может создавать оборудование."""
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse('api:v1:procurement:equipment-list')
|
||||
|
||||
url = reverse("api:v1:procurement:equipment-list")
|
||||
|
||||
data = {
|
||||
'name': 'Тестовое оборудование',
|
||||
'inventory_number': 'INV-2026-999',
|
||||
'category': category.id,
|
||||
'department': department.id,
|
||||
'purchase_date': '2026-02-01',
|
||||
'purchase_cost': '10000.00',
|
||||
"name": "Тестовое оборудование",
|
||||
"inventory_number": "INV-2026-999",
|
||||
"category": category.id,
|
||||
"department": department.id,
|
||||
"purchase_date": "2026-02-01",
|
||||
"purchase_cost": "10000.00",
|
||||
}
|
||||
|
||||
response = api_client.post(url, data, format='json')
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_create_equipment_duplicate_inventory_number(
|
||||
@ -251,20 +237,20 @@ class TestEquipmentCreate:
|
||||
):
|
||||
"""Нельзя создать с дублирующим инвентарным номером."""
|
||||
api_client.force_authenticate(user=staff_user)
|
||||
url = reverse('api:v1:procurement:equipment-list')
|
||||
|
||||
url = reverse("api:v1:procurement:equipment-list")
|
||||
|
||||
data = {
|
||||
'name': 'Другое оборудование',
|
||||
'inventory_number': equipment.inventory_number, # Дубликат!
|
||||
'category': category.id,
|
||||
'department': department.id,
|
||||
'purchase_date': '2026-02-01',
|
||||
'purchase_cost': '10000.00',
|
||||
"name": "Другое оборудование",
|
||||
"inventory_number": equipment.inventory_number, # Дубликат!
|
||||
"category": category.id,
|
||||
"department": department.id,
|
||||
"purchase_date": "2026-02-01",
|
||||
"purchase_cost": "10000.00",
|
||||
}
|
||||
|
||||
response = api_client.post(url, data, format='json')
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data['inventory_number'] != equipment.inventory_number
|
||||
assert response.data["inventory_number"] != equipment.inventory_number
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
@ -275,27 +261,22 @@ class TestEquipmentCreate:
|
||||
class TestEquipmentDetail:
|
||||
"""Тесты получения детальной информации об оборудовании."""
|
||||
|
||||
def test_retrieve_equipment(
|
||||
self, api_client, user, equipment
|
||||
):
|
||||
def test_retrieve_equipment(self, api_client, user, equipment):
|
||||
"""Получение детальной информации."""
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:equipment-detail',
|
||||
kwargs={'pk': equipment.id}
|
||||
"api:v1:procurement:equipment-detail", kwargs={"pk": equipment.id}
|
||||
)
|
||||
response = api_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data['id'] == equipment.id
|
||||
assert response.data['name'] == equipment.name
|
||||
assert response.data['inventory_number'] == equipment.inventory_number
|
||||
assert 'is_under_warranty' in response.data
|
||||
assert response.data['is_under_warranty'] is True
|
||||
|
||||
def test_retrieve_with_maintenance_history(
|
||||
self, api_client, user, equipment
|
||||
):
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["id"] == equipment.id
|
||||
assert response.data["name"] == equipment.name
|
||||
assert response.data["inventory_number"] == equipment.inventory_number
|
||||
assert "is_under_warranty" in response.data
|
||||
assert response.data["is_under_warranty"] is True
|
||||
|
||||
def test_retrieve_with_maintenance_history(self, api_client, user, equipment):
|
||||
"""Детальная информация включает историю ТО."""
|
||||
# Создаем записи об обслуживании
|
||||
MaintenanceRecord.objects.create(
|
||||
@ -306,16 +287,15 @@ class TestEquipmentDetail:
|
||||
cost=Decimal("5000.00"),
|
||||
performed_by=user,
|
||||
)
|
||||
|
||||
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:equipment-detail',
|
||||
kwargs={'pk': equipment.id}
|
||||
"api:v1:procurement:equipment-detail", kwargs={"pk": equipment.id}
|
||||
)
|
||||
response = api_client.get(url)
|
||||
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data['maintenance_count'] >= 1
|
||||
assert response.data["maintenance_count"] >= 1
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
@ -326,40 +306,34 @@ class TestEquipmentDetail:
|
||||
class TestEquipmentUpdate:
|
||||
"""Тесты обновления оборудования."""
|
||||
|
||||
def test_update_equipment_as_staff(
|
||||
self, api_client, staff_user, equipment
|
||||
):
|
||||
def test_update_equipment_as_staff(self, api_client, staff_user, equipment):
|
||||
"""Обновление оборудования staff пользователем."""
|
||||
api_client.force_authenticate(user=staff_user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:equipment-detail',
|
||||
kwargs={'pk': equipment.id}
|
||||
"api:v1:procurement:equipment-detail", kwargs={"pk": equipment.id}
|
||||
)
|
||||
|
||||
data = {
|
||||
'location': 'Офис 4.01',
|
||||
'notes': 'Перемещено в новый офис',
|
||||
}
|
||||
response = api_client.patch(url, data, format='json')
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data['location'] == 'Офис 4.01'
|
||||
assert 'новый офис' in response.data['notes'].lower()
|
||||
|
||||
def test_update_equipment_regular_user_forbidden(
|
||||
self, api_client, user, equipment
|
||||
):
|
||||
data = {
|
||||
"location": "Офис 4.01",
|
||||
"notes": "Перемещено в новый офис",
|
||||
}
|
||||
response = api_client.patch(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["location"] == "Офис 4.01"
|
||||
assert "новый офис" in response.data["notes"].lower()
|
||||
|
||||
def test_update_equipment_regular_user_forbidden(self, api_client, user, equipment):
|
||||
"""Ответственный пользователь может обновлять своё оборудование."""
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:equipment-detail',
|
||||
kwargs={'pk': equipment.id}
|
||||
"api:v1:procurement:equipment-detail", kwargs={"pk": equipment.id}
|
||||
)
|
||||
|
||||
data = {'location': 'Новое место'}
|
||||
response = api_client.patch(url, data, format='json')
|
||||
|
||||
data = {"location": "Новое место"}
|
||||
response = api_client.patch(url, data, format="json")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data['location'] == 'Новое место'
|
||||
assert response.data["location"] == "Новое место"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
@ -370,9 +344,7 @@ class TestEquipmentUpdate:
|
||||
class TestEquipmentActions:
|
||||
"""Тесты специальных действий с оборудованием."""
|
||||
|
||||
def test_transfer_equipment(
|
||||
self, api_client, staff_user, equipment, db
|
||||
):
|
||||
def test_transfer_equipment(self, api_client, staff_user, equipment, db):
|
||||
"""Передача оборудования."""
|
||||
new_user = Employee.objects.create_user(
|
||||
email="newuser@example.com",
|
||||
@ -380,28 +352,29 @@ class TestEquipmentActions:
|
||||
phone_number="+79995555555",
|
||||
first_name="Новый",
|
||||
last_name="Пользователь",
|
||||
is_active=True,
|
||||
send_activation_email=False,
|
||||
)
|
||||
|
||||
|
||||
api_client.force_authenticate(user=staff_user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:equipment-transfer',
|
||||
kwargs={'pk': equipment.id}
|
||||
"api:v1:procurement:equipment-transfer", kwargs={"pk": equipment.id}
|
||||
)
|
||||
|
||||
|
||||
data = {
|
||||
'to_person': new_user.id,
|
||||
'to_location': 'Офис 2.10',
|
||||
'reason': 'Перевод в другой отдел',
|
||||
"to_person": new_user.id,
|
||||
"to_location": "Офис 2.10",
|
||||
"reason": "Перевод в другой отдел",
|
||||
}
|
||||
response = api_client.post(url, data, format='json')
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
|
||||
# Проверяем что ответственный изменился
|
||||
equipment.refresh_from_db()
|
||||
assert equipment.responsible_person == new_user
|
||||
assert equipment.location == 'Офис 2.10'
|
||||
assert equipment.location == "Офис 2.10"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# ТЕСТЫ КАТЕГОРИЙ
|
||||
@ -411,47 +384,41 @@ class TestEquipmentActions:
|
||||
class TestEquipmentCategories:
|
||||
"""Тесты API категорий оборудования."""
|
||||
|
||||
def test_list_categories(
|
||||
self, api_client, user, category
|
||||
):
|
||||
def test_list_categories(self, api_client, user, category):
|
||||
"""Список категорий."""
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse('api:v1:procurement:equipmentcategory-list')
|
||||
url = reverse("api:v1:procurement:equipmentcategory-list")
|
||||
response = api_client.get(url)
|
||||
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.data) >= 1
|
||||
|
||||
def test_create_category_as_staff(
|
||||
self, api_client, staff_user
|
||||
):
|
||||
def test_create_category_as_staff(self, api_client, staff_user):
|
||||
"""Создание категории staff пользователем."""
|
||||
api_client.force_authenticate(user=staff_user)
|
||||
url = reverse('api:v1:procurement:equipmentcategory-list')
|
||||
|
||||
data = {
|
||||
'name': 'Мониторы',
|
||||
'description': 'Все виды мониторов',
|
||||
'icon': 'bi-display',
|
||||
}
|
||||
response = api_client.post(url, data, format='json')
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data['name'] == 'Мониторы'
|
||||
url = reverse("api:v1:procurement:equipmentcategory-list")
|
||||
|
||||
def test_hierarchical_categories(
|
||||
self, api_client, staff_user, category
|
||||
):
|
||||
data = {
|
||||
"name": "Мониторы",
|
||||
"description": "Все виды мониторов",
|
||||
"icon": "bi-display",
|
||||
}
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data["name"] == "Мониторы"
|
||||
|
||||
def test_hierarchical_categories(self, api_client, staff_user, category):
|
||||
"""Создание подкатегории."""
|
||||
api_client.force_authenticate(user=staff_user)
|
||||
url = reverse('api:v1:procurement:equipmentcategory-list')
|
||||
|
||||
url = reverse("api:v1:procurement:equipmentcategory-list")
|
||||
|
||||
data = {
|
||||
'name': 'Игровые ноутбуки',
|
||||
'parent': category.id,
|
||||
'description': 'Высокопроизводительные ноутбуки',
|
||||
"name": "Игровые ноутбуки",
|
||||
"parent": category.id,
|
||||
"description": "Высокопроизводительные ноутбуки",
|
||||
}
|
||||
response = api_client.post(url, data, format='json')
|
||||
|
||||
response = api_client.post(url, data, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data['parent'] == category.id
|
||||
assert response.data["parent"] == category.id
|
||||
|
||||
@ -31,6 +31,7 @@ from finance.models import (
|
||||
PayrollTableReviewMark,
|
||||
PayrollWorkRecord,
|
||||
)
|
||||
from finance.payroll.config import DEFAULT_CONFIG
|
||||
from requests_app.enums import RequestStatus, RequestType
|
||||
from requests_app.models import Request
|
||||
|
||||
@ -394,9 +395,7 @@ def test_copy_input_lines_previews_scope_and_handles_existing_values(
|
||||
assert component_preview[correction_credit.pk]["line_count"] == 0
|
||||
assert component_preview[correction_credit.pk]["employee_count"] == 0
|
||||
assert component_preview[correction_credit.pk]["amount_total"] == "0.00"
|
||||
employee_preview = {
|
||||
item["id"]: item for item in body["employees"]
|
||||
}
|
||||
employee_preview = {item["id"]: item for item in body["employees"]}
|
||||
assert employee_preview[active_employee.pk]["is_active"] is True
|
||||
assert employee_preview[inactive_employee.pk]["is_active"] is False
|
||||
assert employee_preview[active_employee.pk]["existing_component_counts"] == {
|
||||
@ -425,10 +424,13 @@ def test_copy_input_lines_previews_scope_and_handles_existing_values(
|
||||
"replaced_cells": 0,
|
||||
"voided": 0,
|
||||
}
|
||||
assert PayrollInputLine.objects.filter(
|
||||
period=target,
|
||||
employee=inactive_employee,
|
||||
).count() == 0
|
||||
assert (
|
||||
PayrollInputLine.objects.filter(
|
||||
period=target,
|
||||
employee=inactive_employee,
|
||||
).count()
|
||||
== 0
|
||||
)
|
||||
|
||||
replacement_key = uuid.uuid4()
|
||||
replaced = client.post(
|
||||
@ -861,9 +863,7 @@ def test_workspace_contract_is_minimal_and_redacts_money_without_view_all(
|
||||
assert inactive_employee.pk not in {item["id"] for item in body["employees"]}
|
||||
assert inactive_employee.pk in {item["id"] for item in body["rate_employees"]}
|
||||
assert hidden_employee.pk not in {item["id"] for item in body["employees"]}
|
||||
assert hidden_employee.pk not in {
|
||||
item["id"] for item in body["rate_employees"]
|
||||
}
|
||||
assert hidden_employee.pk not in {item["id"] for item in body["rate_employees"]}
|
||||
|
||||
|
||||
def test_workspace_readiness_identifies_affected_employees_and_draft_counts(
|
||||
@ -1018,9 +1018,7 @@ def test_period_table_lists_all_employees_and_requires_view_all(
|
||||
assert response.status_code == 200, response.content
|
||||
assert_no_store(response)
|
||||
body = response.json()
|
||||
assert hidden_employee.pk not in {
|
||||
item["employee"]["id"] for item in body["rows"]
|
||||
}
|
||||
assert hidden_employee.pk not in {item["employee"]["id"] for item in body["rows"]}
|
||||
assert body["period_id"] == period.pk
|
||||
assert body["calculation_rules"]["point_policy"] == "proportional_with_excess"
|
||||
assert body["calculation_rules"]["money_quantum"] == "0.01"
|
||||
@ -1051,9 +1049,7 @@ def test_period_table_lists_all_employees_and_requires_view_all(
|
||||
assert row["opening_balance"] == "0"
|
||||
assert row["closing_balance"] == "0.00"
|
||||
assert row["totals_preliminary"] is True
|
||||
assert all(
|
||||
item["employee_id"] != employee.pk for item in body["carryovers"]
|
||||
)
|
||||
assert all(item["employee_id"] != employee.pk for item in body["carryovers"])
|
||||
assert body["summary"]["preliminary_count"] == body["summary"]["employee_count"]
|
||||
missing_row = next(
|
||||
item for item in body["rows"] if item["employee"]["id"] == missing_employee.pk
|
||||
@ -1179,17 +1175,23 @@ def test_period_table_review_marks_are_personal_and_idempotent(
|
||||
assert response.status_code == 200, response.content
|
||||
assert response.json() == {"employee_ids": [employee.pk]}
|
||||
|
||||
assert PayrollTableReviewMark.objects.filter(
|
||||
period=period,
|
||||
reviewer=first_reviewer,
|
||||
employee=employee,
|
||||
).count() == 1
|
||||
assert (
|
||||
PayrollTableReviewMark.objects.filter(
|
||||
period=period,
|
||||
reviewer=first_reviewer,
|
||||
employee=employee,
|
||||
).count()
|
||||
== 1
|
||||
)
|
||||
assert first_client.get(api_url("admin-period-table", pk=period.pk)).json()[
|
||||
"review_mark_employee_ids"
|
||||
] == [employee.pk]
|
||||
assert auth_client_factory(second_reviewer).get(
|
||||
api_url("admin-period-table", pk=period.pk)
|
||||
).json()["review_mark_employee_ids"] == []
|
||||
assert (
|
||||
auth_client_factory(second_reviewer)
|
||||
.get(api_url("admin-period-table", pk=period.pk))
|
||||
.json()["review_mark_employee_ids"]
|
||||
== []
|
||||
)
|
||||
|
||||
cleared = first_client.delete(url)
|
||||
assert cleared.status_code == 200, cleared.content
|
||||
@ -2148,7 +2150,8 @@ def test_workspace_blocks_period_outside_ruleset_effective_dates(
|
||||
"code": "RULESET_NOT_EFFECTIVE",
|
||||
"message": (
|
||||
"Для выбранного периода нет действующих правил расчёта. "
|
||||
"Набор eusrr-standard, версия 2026.08.1, применяется с 01.07.2026. "
|
||||
f"Набор eusrr-standard, версия {DEFAULT_CONFIG['RULESET_VERSION']}, "
|
||||
"применяется с 01.07.2026. "
|
||||
"Измените период или подключите историческую версию правил."
|
||||
),
|
||||
"details": {
|
||||
@ -2158,7 +2161,7 @@ def test_workspace_blocks_period_outside_ruleset_effective_dates(
|
||||
},
|
||||
"ruleset": {
|
||||
"id": "eusrr-standard",
|
||||
"version": "2026.08.1",
|
||||
"version": DEFAULT_CONFIG["RULESET_VERSION"],
|
||||
"effective_from": "2026-07-01",
|
||||
},
|
||||
},
|
||||
@ -3285,7 +3288,9 @@ def test_staff_admin_completes_entire_payroll_workflow_with_one_command(
|
||||
for row in table_after_confirmation.json()["rows"]
|
||||
if row["employee"]["id"] == employee.pk
|
||||
)
|
||||
assert confirmed_row["payment_confirmation"]["confirmed_by"]["id"] == administrator.pk
|
||||
assert (
|
||||
confirmed_row["payment_confirmation"]["confirmed_by"]["id"] == administrator.pk
|
||||
)
|
||||
|
||||
revoked = administrator_client.post(
|
||||
api_url(
|
||||
|
||||
@ -32,6 +32,7 @@ from finance.models import (
|
||||
PayrollWorkRecord,
|
||||
)
|
||||
from finance.payroll.exceptions import PayrollOperationError, PayrollPermissionDenied
|
||||
from finance.payroll.config import DEFAULT_CONFIG
|
||||
from finance.payroll.services import (
|
||||
acknowledge_statement,
|
||||
approve_input_line,
|
||||
@ -230,7 +231,9 @@ def test_only_manual_corrections_move_between_periods_and_can_be_settled(
|
||||
(june_payment, approve_input_line),
|
||||
(june_correction, approve_input_line),
|
||||
):
|
||||
approve(instance.pk, actor=approver, expected_lock_version=instance.lock_version)
|
||||
approve(
|
||||
instance.pk, actor=approver, expected_lock_version=instance.lock_version
|
||||
)
|
||||
|
||||
june_run = calculate_period(june_period.pk, actor=operator)
|
||||
june_statement = june_run.statements.get(employee=employee)
|
||||
@ -277,7 +280,9 @@ def test_only_manual_corrections_move_between_periods_and_can_be_settled(
|
||||
(july_payment, approve_input_line),
|
||||
(july_correction, approve_input_line),
|
||||
):
|
||||
approve(instance.pk, actor=approver, expected_lock_version=instance.lock_version)
|
||||
approve(
|
||||
instance.pk, actor=approver, expected_lock_version=instance.lock_version
|
||||
)
|
||||
|
||||
july_run = calculate_period(july_period.pk, actor=operator)
|
||||
july_statement = july_run.statements.get(employee=employee)
|
||||
@ -326,7 +331,9 @@ def test_only_manual_corrections_move_between_periods_and_can_be_settled(
|
||||
(august_payment, approve_input_line),
|
||||
(august_correction, approve_input_line),
|
||||
):
|
||||
approve(instance.pk, actor=approver, expected_lock_version=instance.lock_version)
|
||||
approve(
|
||||
instance.pk, actor=approver, expected_lock_version=instance.lock_version
|
||||
)
|
||||
|
||||
august_run = calculate_period(august_period.pk, actor=operator)
|
||||
august_statement = august_run.statements.get(employee=employee)
|
||||
@ -740,7 +747,8 @@ def test_ruleset_not_effective_keeps_domain_code_and_localized_message(
|
||||
assert error.value.code == "RULESET_NOT_EFFECTIVE"
|
||||
assert error.value.message == (
|
||||
"Для выбранного периода нет действующих правил расчёта. "
|
||||
"Набор eusrr-standard, версия 2026.08.1, применяется с 01.07.2026. "
|
||||
f"Набор eusrr-standard, версия {DEFAULT_CONFIG['RULESET_VERSION']}, "
|
||||
"применяется с 01.07.2026. "
|
||||
"Измените период или подключите историческую версию правил."
|
||||
)
|
||||
assert error.value.details["employee_id"] == payroll_users["employee"].pk
|
||||
@ -750,7 +758,7 @@ def test_ruleset_not_effective_keeps_domain_code_and_localized_message(
|
||||
}
|
||||
assert error.value.details["ruleset"] == {
|
||||
"id": "eusrr-standard",
|
||||
"version": "2026.08.1",
|
||||
"version": DEFAULT_CONFIG["RULESET_VERSION"],
|
||||
"effective_from": "2026-07-01",
|
||||
}
|
||||
assert {issue["code"] for issue in error.value.details["issues"]} == {
|
||||
|
||||
@ -11,6 +11,9 @@ pytestmark = pytest.mark.django_db(transaction=True)
|
||||
def _migrate_to(target):
|
||||
executor = MigrationExecutor(connection)
|
||||
executor.loader.build_graph()
|
||||
target_apps = {app_label for app_label, _ in target}
|
||||
if "employees" not in target_apps:
|
||||
target = [*target, *executor.loader.graph.leaf_nodes("employees")]
|
||||
executor.migrate(target)
|
||||
return executor.loader.project_state(target).apps
|
||||
|
||||
|
||||
@ -39,12 +39,17 @@ def test_migration_0002_backfills_department_binding_by_slug():
|
||||
slug=f"department-{department.id}",
|
||||
)
|
||||
|
||||
_migrate_to([("scheduling", "0002_calendarbinding")])
|
||||
|
||||
from scheduling.models import CalendarBinding
|
||||
new_apps = _migrate_to(
|
||||
[
|
||||
("employees", "0047_alter_departmentpermission_code"),
|
||||
("schedule", "0015_rename_indexes"),
|
||||
("scheduling", "0002_calendarbinding"),
|
||||
]
|
||||
)
|
||||
CalendarBinding = new_apps.get_model("scheduling", "CalendarBinding")
|
||||
|
||||
binding = CalendarBinding.objects.get(calendar_id=calendar.id)
|
||||
assert binding.type == CalendarBinding.BindingType.DEPARTMENT
|
||||
assert binding.type == "department"
|
||||
assert binding.context_object_id == department.id
|
||||
assert binding.context_content_type.app_label == "employees"
|
||||
assert binding.context_content_type.model == "department"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user