""" Тесты для API модуля закупок. """ from decimal import Decimal import pytest from django.urls import reverse from django.utils import timezone from rest_framework import status from rest_framework.test import APIClient from employees.models import Department, Employee from procurement.constants import ProcurementStatus, UrgencyLevel from procurement.models import ( Budget, Equipment, EquipmentCategory, ProcurementItem, ProcurementRequest, ProcurementRequestView, Supplier, ) @pytest.fixture def api_client(): """API клиент.""" return APIClient() @pytest.fixture def user(db): """Обычный пользователь.""" return Employee.objects.create_user( email="user@example.com", password="testpass123", phone_number="+79991111111", first_name="Иван", last_name="Иванов", send_activation_email=False, ) @pytest.fixture def staff_user(db): """Staff пользователь.""" return Employee.objects.create_user( email="staff@example.com", password="testpass123", phone_number="+79992222222", first_name="Админ", last_name="Админов", is_staff=True, send_activation_email=False, ) @pytest.fixture def department(db): """Тестовый отдел.""" return Department.objects.create(name="Тестовый отдел", description="Для тестов") @pytest.fixture def procurement_request(db, department, user): """Тестовая заявка.""" return ProcurementRequest.objects.create( title="Тестовая заявка", description="Описание", department=department, requestor=user, status=ProcurementStatus.DRAFT, urgency=UrgencyLevel.MEDIUM, ) @pytest.mark.django_db class TestProcurementRequestAPI: """Тесты API заявок на закупку.""" def test_list_requests_unauthorized(self, api_client): """Тест: неавторизованный доступ запрещен.""" url = reverse("api:v1:procurement:procurementrequest-list") response = api_client.get(url) assert response.status_code == status.HTTP_401_UNAUTHORIZED def test_list_requests_authorized(self, api_client, user, procurement_request): """Тест: авторизованный пользователь видит свои заявки.""" api_client.force_authenticate(user=user) url = reverse("api:v1:procurement:procurementrequest-list") response = api_client.get(url) assert response.status_code == status.HTTP_200_OK assert len(response.data["results"]) == 1 assert response.data["results"][0]["title"] == "Тестовая заявка" def test_list_completed_request_includes_completed_at( self, api_client, user, procurement_request ): """Тест: список отдаёт дату завершения для завершённых заявок.""" procurement_request.status = ProcurementStatus.COMPLETED procurement_request.completed_at = timezone.now() procurement_request.save(update_fields=["status", "completed_at"]) api_client.force_authenticate(user=user) url = reverse("api:v1:procurement:procurementrequest-list") response = api_client.get(url) assert response.status_code == status.HTTP_200_OK assert response.data["results"][0]["status"] == ProcurementStatus.COMPLETED assert response.data["results"][0]["completed_at"] is not None def test_create_request(self, api_client, user, department, link_factory): """Тест: создание заявки.""" # Связываем пользователя с отделом (необходимо для создания заявки) link_factory(user, department, is_active=True) api_client.force_authenticate(user=user) url = reverse("api:v1:procurement:procurementrequest-list") data = { "title": "Новая заявка", "description": "Нужны компьютеры", "department": department.id, "urgency": UrgencyLevel.HIGH, "items": [ { "name": "Ноутбук", "quantity": 2, "unit": "шт", "estimated_unit_price": "50000.00", } ], } response = api_client.post(url, data, format="json") assert response.status_code == status.HTTP_201_CREATED assert response.data["title"] == "Новая заявка" assert response.data["requestor"] == user.id def test_retrieve_request(self, api_client, user, procurement_request): """Тест: получение детальной информации о заявке.""" api_client.force_authenticate(user=user) url = reverse( "api:v1:procurement:procurementrequest-detail", kwargs={"pk": procurement_request.id}, ) response = api_client.get(url) assert response.status_code == status.HTTP_200_OK assert response.data["id"] == procurement_request.id assert response.data["title"] == "Тестовая заявка" assert "total_cost" in response.data assert "required_approval_priorities" in response.data assert "is_editable" in response.data def test_list_request_quantity_unit_label_single_unit( self, api_client, user, procurement_request ): """Тест: для одной единицы измерения возвращается ее подпись.""" ProcurementItem.objects.create( request=procurement_request, name="Кабель", quantity=20, unit="м", ) api_client.force_authenticate(user=user) url = reverse("api:v1:procurement:procurementrequest-list") response = api_client.get(url) assert response.status_code == status.HTTP_200_OK assert response.data["results"][0]["quantity_unit_label"] == "метры" def test_list_request_quantity_unit_label_mixed_units( self, api_client, user, procurement_request ): """Тест: для смешанных единиц возвращается обобщенная подпись.""" ProcurementItem.objects.create( request=procurement_request, name="Кабель", quantity=20, unit="м", ) ProcurementItem.objects.create( request=procurement_request, name="Крепеж", quantity=10, unit="шт", ) api_client.force_authenticate(user=user) url = reverse("api:v1:procurement:procurementrequest-list") response = api_client.get(url) assert response.status_code == status.HTTP_200_OK assert response.data["results"][0]["quantity_unit_label"] == "единиц" def test_set_request_viewed_flag(self, api_client, user, procurement_request): """Тест: пользователь может отметить видимую заявку просмотренной.""" api_client.force_authenticate(user=user) url = reverse( "api:v1:procurement:procurementrequest-set-viewed", kwargs={"pk": procurement_request.id}, ) response = api_client.post(url, {"is_viewed": True}, format="json") assert response.status_code == status.HTTP_200_OK assert response.data["is_viewed"] is True view_state = ProcurementRequestView.objects.get( request=procurement_request, user=user, ) assert view_state.is_viewed is True assert view_state.viewed_at is not None response = api_client.post(url, {"is_viewed": False}, format="json") assert response.status_code == status.HTTP_200_OK assert response.data["is_viewed"] is False view_state.refresh_from_db() assert view_state.is_viewed is False assert view_state.viewed_at is None def test_update_own_request_draft(self, api_client, user, procurement_request): """Тест: обновление своей заявки в статусе DRAFT.""" api_client.force_authenticate(user=user) url = reverse( "api:v1:procurement:procurementrequest-detail", kwargs={"pk": procurement_request.id}, ) data = {"title": "Обновленная заявка"} response = api_client.patch(url, data, format="json") assert response.status_code == status.HTTP_200_OK assert response.data["title"] == "Обновленная заявка" @pytest.mark.django_db class TestEquipmentAPI: """Тесты API оборудования.""" def test_list_equipment(self, api_client, user, department): """Тест: список оборудования.""" api_client.force_authenticate(user=user) # Создаем категорию и оборудование category = EquipmentCategory.objects.create(name="Компьютеры") Equipment.objects.create( name="Ноутбук Dell", inventory_number="INV-2025-001", category=category, department=department, purchase_date="2025-01-01", purchase_cost=Decimal("80000.00"), ) url = reverse("api:v1:procurement:equipment-list") response = api_client.get(url) assert response.status_code == status.HTTP_200_OK assert len(response.data["results"]) == 1 def test_create_equipment_staff_only( self, api_client, user, staff_user, department ): """Тест: создание оборудования только для staff.""" category = EquipmentCategory.objects.create(name="Техника") # Обычный пользователь не может создать api_client.force_authenticate(user=user) url = reverse("api:v1:procurement:equipment-list") data = { "name": "Принтер HP", "inventory_number": "INV-2025-002", "category": category.id, "department": department.id, "purchase_date": "2025-01-01", "purchase_cost": "20000.00", } response = api_client.post(url, data, format="json") assert response.status_code == status.HTTP_403_FORBIDDEN # Staff может создать api_client.force_authenticate(user=staff_user) response = api_client.post(url, data, format="json") assert response.status_code == status.HTTP_201_CREATED @pytest.mark.django_db class TestBudgetAPI: """Тесты API бюджетов.""" def test_list_budgets_staff(self, api_client, staff_user, department): """Тест: список бюджетов для staff.""" api_client.force_authenticate(user=staff_user) Budget.objects.create( department=department, year=2025, quarter=4, allocated_amount=Decimal("500000.00"), ) url = reverse("api:v1:procurement:budget-list") response = api_client.get(url) assert response.status_code == status.HTTP_200_OK assert len(response.data["results"]) == 1 def test_list_budgets_regular_user(self, api_client, user): """Тест: обычный пользователь не видит бюджеты (нет прав).""" api_client.force_authenticate(user=user) url = reverse("api:v1:procurement:budget-list") response = api_client.get(url) # Обычный пользователь не имеет доступа к бюджетам assert response.status_code == status.HTTP_403_FORBIDDEN @pytest.mark.django_db class TestSupplierAPI: """Тесты API поставщиков.""" def test_list_suppliers(self, api_client, user): """Тест: список поставщиков.""" api_client.force_authenticate(user=user) Supplier.objects.create( name="ООО Тестовый поставщик", rating=Decimal("4.50"), ) url = reverse("api:v1:procurement:supplier-list") response = api_client.get(url) assert response.status_code == status.HTTP_200_OK assert len(response.data["results"]) == 1 def test_top_rated_suppliers(self, api_client, user): """Тест: лучшие поставщики.""" api_client.force_authenticate(user=user) Supplier.objects.create( name="Отличный поставщик", rating=Decimal("4.80"), is_active=True, ) Supplier.objects.create( name="Плохой поставщик", rating=Decimal("2.50"), is_active=True, ) url = reverse("api:v1:procurement:supplier-top-rated") response = api_client.get(url) assert response.status_code == status.HTTP_200_OK # Должен вернуть только с рейтингом >= 4.0 assert len(response.data) == 1 assert response.data[0]["name"] == "Отличный поставщик" @pytest.mark.django_db class TestEquipmentCategoryAPI: """Тесты API категорий оборудования.""" def test_category_tree(self, api_client, user): """Тест: дерево категорий.""" api_client.force_authenticate(user=user) parent = EquipmentCategory.objects.create(name="Компьютеры") EquipmentCategory.objects.create(name="Ноутбуки", parent=parent) url = reverse("api:v1:procurement:equipmentcategory-tree") response = api_client.get(url) assert response.status_code == status.HTTP_200_OK # Должен вернуть только корневые категории assert len(response.data) == 1 assert response.data[0]["name"] == "Компьютеры"