441 lines
16 KiB
Python
441 lines
16 KiB
Python
"""
|
||
Тесты для workflow согласований модуля закупок.
|
||
"""
|
||
|
||
import pytest
|
||
from django.contrib.auth.models import Permission
|
||
from django.urls import reverse
|
||
from rest_framework import status
|
||
|
||
from procurement.constants import (
|
||
ApprovalStatus,
|
||
ProcurementStatus,
|
||
UrgencyLevel,
|
||
)
|
||
from procurement.models import (
|
||
Approval,
|
||
ApprovalRoute,
|
||
Budget,
|
||
ProcurementItem,
|
||
ProcurementRequest,
|
||
)
|
||
|
||
HEAD_PRIORITY = 1
|
||
FINANCE_PRIORITY = 2
|
||
DIRECTOR_PRIORITY = 3
|
||
|
||
|
||
@pytest.mark.django_db
|
||
class TestProcurementWorkflow:
|
||
"""Тесты workflow согласований."""
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def setup(self, api_client, department_factory, user_factory, link_factory):
|
||
"""Подготовка данных для тестов."""
|
||
# Создаем отдел
|
||
self.department = department_factory(name="IT отдел")
|
||
|
||
# Создаем пользователей
|
||
self.requestor = user_factory(
|
||
email="requestor@test.com",
|
||
first_name="Иван",
|
||
last_name="Иванов",
|
||
)
|
||
link_factory(self.requestor, self.department, is_active=True)
|
||
|
||
self.dept_head = user_factory(
|
||
email="head@test.com",
|
||
first_name="Петр",
|
||
last_name="Петров",
|
||
)
|
||
self.department.head = self.dept_head
|
||
self.department.save()
|
||
|
||
self.finance = user_factory(
|
||
email="finance@test.com",
|
||
first_name="Мария",
|
||
last_name="Сидорова",
|
||
)
|
||
# Даем права на бюджеты
|
||
perm = Permission.objects.get(codename="change_budget")
|
||
self.finance.user_permissions.add(perm)
|
||
|
||
self.director = user_factory(
|
||
email="director@test.com",
|
||
first_name="Сергей",
|
||
last_name="Сергеев",
|
||
superuser=True,
|
||
)
|
||
|
||
ApprovalRoute.objects.create(
|
||
priority=HEAD_PRIORITY,
|
||
resolver_type=ApprovalRoute.ResolverType.DEPARTMENT_HEAD,
|
||
)
|
||
ApprovalRoute.objects.create(
|
||
priority=FINANCE_PRIORITY,
|
||
min_amount=10000,
|
||
resolver_type=ApprovalRoute.ResolverType.FIXED_EMPLOYEE,
|
||
employee=self.finance,
|
||
)
|
||
ApprovalRoute.objects.create(
|
||
priority=DIRECTOR_PRIORITY,
|
||
min_amount=50000,
|
||
resolver_type=ApprovalRoute.ResolverType.FIXED_EMPLOYEE,
|
||
employee=self.director,
|
||
)
|
||
|
||
# Создаем бюджет
|
||
Budget.objects.create(
|
||
department=self.department,
|
||
year=2025,
|
||
quarter=4,
|
||
allocated_amount=100000,
|
||
spent_amount=0,
|
||
)
|
||
|
||
self.client = api_client
|
||
|
||
def test_create_draft_request(self):
|
||
"""Тест создания черновика заявки."""
|
||
self.client.force_authenticate(user=self.requestor)
|
||
|
||
data = {
|
||
"title": "Закупка ноутбуков",
|
||
"description": "Нужны новые ноутбуки для разработчиков",
|
||
"department": self.department.id,
|
||
"urgency": UrgencyLevel.MEDIUM,
|
||
"items": [
|
||
{
|
||
"name": "Ноутбук Dell XPS 15",
|
||
"quantity": 2,
|
||
"unit": "шт",
|
||
"estimated_unit_price": 80000,
|
||
}
|
||
],
|
||
}
|
||
|
||
response = self.client.post(
|
||
reverse("api:v1:procurement:procurementrequest-list"), data, format="json"
|
||
)
|
||
|
||
assert response.status_code == status.HTTP_201_CREATED
|
||
# Проверяем, что заявка создана
|
||
assert "title" in response.data
|
||
assert response.data["title"] == "Закупка ноутбуков"
|
||
|
||
# Проверяем в базе данных
|
||
request = ProcurementRequest.objects.get(title="Закупка ноутбуков")
|
||
assert request.status == ProcurementStatus.DRAFT
|
||
assert request.is_editable is True
|
||
assert request.items.count() == 1
|
||
|
||
def test_submit_request_creates_approvals(self):
|
||
"""Тест отправки заявки создает записи согласований."""
|
||
# Создаем заявку
|
||
request = ProcurementRequest.objects.create(
|
||
title="Закупка оборудования",
|
||
description="Тестовая заявка",
|
||
department=self.department,
|
||
processing_department=self.department,
|
||
requestor=self.requestor,
|
||
status=ProcurementStatus.WAITING,
|
||
urgency=UrgencyLevel.MEDIUM,
|
||
)
|
||
ProcurementItem.objects.create(
|
||
request=request,
|
||
name="Монитор",
|
||
quantity=1,
|
||
unit="шт",
|
||
estimated_unit_price=15000,
|
||
)
|
||
|
||
self.client.force_authenticate(user=self.requestor)
|
||
submit_url = reverse(
|
||
"api:v1:procurement:procurementrequest-submit",
|
||
kwargs={"pk": request.id},
|
||
)
|
||
|
||
response = self.client.post(submit_url)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
request.refresh_from_db()
|
||
assert request.status == ProcurementStatus.PENDING
|
||
|
||
# Проверяем создание согласований
|
||
approvals = request.approvals.all()
|
||
# Для суммы 15,000 нужно 2 согласования
|
||
# (руководитель + финансы)
|
||
assert approvals.count() == 2
|
||
|
||
def test_cannot_submit_without_items(self):
|
||
"""Тест: нельзя отправить заявку без позиций."""
|
||
request = ProcurementRequest.objects.create(
|
||
title="Пустая заявка",
|
||
description="Без позиций",
|
||
department=self.department,
|
||
processing_department=self.department,
|
||
requestor=self.requestor,
|
||
status=ProcurementStatus.WAITING,
|
||
urgency=UrgencyLevel.LOW,
|
||
)
|
||
|
||
self.client.force_authenticate(user=self.requestor)
|
||
submit_url = reverse(
|
||
"api:v1:procurement:procurementrequest-submit",
|
||
kwargs={"pk": request.id},
|
||
)
|
||
|
||
response = self.client.post(submit_url)
|
||
|
||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||
assert "позиц" in response.data["error"].lower()
|
||
|
||
def test_submit_fails_when_required_authority_missing(self):
|
||
"""Тест: нельзя отправить заявку без настроенного обязательного согласующего."""
|
||
self.director.is_active = False
|
||
self.director.save(update_fields=["is_active"])
|
||
|
||
request = ProcurementRequest.objects.create(
|
||
title="Дорогая заявка",
|
||
description="Нет директора для согласования",
|
||
department=self.department,
|
||
processing_department=self.department,
|
||
requestor=self.requestor,
|
||
status=ProcurementStatus.WAITING,
|
||
urgency=UrgencyLevel.HIGH,
|
||
)
|
||
ProcurementItem.objects.create(
|
||
request=request,
|
||
name="Очень дорогое оборудование",
|
||
quantity=1,
|
||
unit="шт",
|
||
estimated_unit_price=200000,
|
||
)
|
||
|
||
self.client.force_authenticate(user=self.requestor)
|
||
submit_url = reverse(
|
||
"api:v1:procurement:procurementrequest-submit",
|
||
kwargs={"pk": request.id},
|
||
)
|
||
|
||
response = self.client.post(submit_url)
|
||
|
||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||
assert "согласующ" in response.data["error"].lower()
|
||
assert DIRECTOR_PRIORITY in response.data["missing_priorities"]
|
||
|
||
def test_approve_by_department_head(self):
|
||
"""Тест одобрения заявки руководителем отдела."""
|
||
# Создаем заявку и отправляем на согласование
|
||
request = ProcurementRequest.objects.create(
|
||
title="Заявка на согласование",
|
||
description="Тест",
|
||
department=self.department,
|
||
requestor=self.requestor,
|
||
status=ProcurementStatus.PENDING,
|
||
urgency=UrgencyLevel.MEDIUM,
|
||
)
|
||
ProcurementItem.objects.create(
|
||
request=request,
|
||
name="Клавиатура",
|
||
quantity=1,
|
||
unit="шт",
|
||
estimated_unit_price=3000,
|
||
)
|
||
|
||
# Создаем согласование для руководителя
|
||
Approval.objects.create(
|
||
request=request,
|
||
approver=self.dept_head,
|
||
priority=HEAD_PRIORITY,
|
||
status=ApprovalStatus.PENDING,
|
||
)
|
||
|
||
self.client.force_authenticate(user=self.dept_head)
|
||
approve_url = reverse(
|
||
"api:v1:procurement:procurementrequest-approve",
|
||
kwargs={"pk": request.id},
|
||
)
|
||
|
||
response = self.client.post(
|
||
approve_url,
|
||
{"comment": "Одобряю"},
|
||
)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
|
||
# Проверяем статус согласования
|
||
approval = request.approvals.get(approver=self.dept_head)
|
||
assert approval.status == ApprovalStatus.APPROVED
|
||
assert approval.comment == "Одобряю"
|
||
|
||
# Проверяем статус заявки (должна быть одобрена)
|
||
request.refresh_from_db()
|
||
assert request.status == ProcurementStatus.APPROVED
|
||
|
||
def test_reject_by_approver(self):
|
||
"""Тест отклонения заявки согласующим."""
|
||
request = ProcurementRequest.objects.create(
|
||
title="Заявка на отклонение",
|
||
description="Будет отклонена",
|
||
department=self.department,
|
||
requestor=self.requestor,
|
||
status=ProcurementStatus.PENDING,
|
||
urgency=UrgencyLevel.LOW,
|
||
)
|
||
|
||
Approval.objects.create(
|
||
request=request,
|
||
approver=self.dept_head,
|
||
priority=HEAD_PRIORITY,
|
||
status=ApprovalStatus.PENDING,
|
||
)
|
||
|
||
self.client.force_authenticate(user=self.dept_head)
|
||
reject_url = reverse(
|
||
"api:v1:procurement:procurementrequest-reject",
|
||
kwargs={"pk": request.id},
|
||
)
|
||
|
||
response = self.client.post(
|
||
reject_url,
|
||
{"comment": "Не обоснована необходимость"},
|
||
)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
|
||
# Проверяем статус согласования
|
||
approval = request.approvals.get(approver=self.dept_head)
|
||
assert approval.status == ApprovalStatus.REJECTED
|
||
assert "обоснована" in approval.comment
|
||
|
||
# Проверяем статус заявки
|
||
request.refresh_from_db()
|
||
assert request.status == ProcurementStatus.REJECTED
|
||
|
||
def test_multi_level_approval(self):
|
||
"""Тест многоуровневого согласования."""
|
||
# Создаем заявку на 45,000₽ (нужно 2 уровня)
|
||
request = ProcurementRequest.objects.create(
|
||
title="Средняя по стоимости заявка",
|
||
description="Требует 2 согласования",
|
||
department=self.department,
|
||
requestor=self.requestor,
|
||
status=ProcurementStatus.PENDING,
|
||
urgency=UrgencyLevel.MEDIUM,
|
||
)
|
||
ProcurementItem.objects.create(
|
||
request=request,
|
||
name="Сервер",
|
||
quantity=1,
|
||
unit="шт",
|
||
estimated_unit_price=45000,
|
||
)
|
||
|
||
# Создаем 2 согласования
|
||
Approval.objects.create(
|
||
request=request,
|
||
approver=self.dept_head,
|
||
priority=HEAD_PRIORITY,
|
||
status=ApprovalStatus.PENDING,
|
||
)
|
||
Approval.objects.create(
|
||
request=request,
|
||
approver=self.finance,
|
||
priority=FINANCE_PRIORITY,
|
||
status=ApprovalStatus.PENDING,
|
||
)
|
||
|
||
# 1. Одобряет руководитель отдела
|
||
self.client.force_authenticate(user=self.dept_head)
|
||
approve_url = reverse(
|
||
"api:v1:procurement:procurementrequest-approve",
|
||
kwargs={"pk": request.id},
|
||
)
|
||
response = self.client.post(
|
||
approve_url,
|
||
{"comment": "От отдела одобряю"},
|
||
)
|
||
assert response.status_code == status.HTTP_200_OK
|
||
|
||
request.refresh_from_db()
|
||
# Заявка еще на согласовании (не все одобрили)
|
||
assert request.status == ProcurementStatus.PENDING
|
||
|
||
# 2. Одобряет финансовый менеджер
|
||
self.client.force_authenticate(user=self.finance)
|
||
response = self.client.post(
|
||
approve_url,
|
||
{"comment": "От финансов одобряю"},
|
||
)
|
||
print(f"DEBUG 2: Response status: {response.status_code}")
|
||
if response.status_code != status.HTTP_200_OK:
|
||
print(f"DEBUG 2: Response data: {response.data}")
|
||
assert response.status_code == status.HTTP_200_OK
|
||
|
||
request.refresh_from_db()
|
||
# Теперь заявка одобрена
|
||
assert request.status == ProcurementStatus.APPROVED
|
||
|
||
def test_cannot_approve_without_permission(self):
|
||
"""Тест: нельзя согласовать без прав."""
|
||
request = ProcurementRequest.objects.create(
|
||
title="Чужая заявка",
|
||
description="Не могу согласовать",
|
||
department=self.department,
|
||
requestor=self.requestor,
|
||
status=ProcurementStatus.PENDING,
|
||
urgency=UrgencyLevel.LOW,
|
||
)
|
||
|
||
Approval.objects.create(
|
||
request=request,
|
||
approver=self.dept_head,
|
||
priority=HEAD_PRIORITY,
|
||
status=ApprovalStatus.PENDING,
|
||
)
|
||
|
||
# Пытаемся одобрить от лица другого пользователя
|
||
self.client.force_authenticate(user=self.finance)
|
||
approve_url = reverse(
|
||
"api:v1:procurement:procurementrequest-approve",
|
||
kwargs={"pk": request.id},
|
||
)
|
||
|
||
response = self.client.post(approve_url)
|
||
|
||
# Должна быть ошибка - 404 или 403/400
|
||
# 404 если заявка не в queryset, 403/400 если нет прав
|
||
assert response.status_code in [
|
||
status.HTTP_404_NOT_FOUND,
|
||
status.HTTP_403_FORBIDDEN,
|
||
status.HTTP_400_BAD_REQUEST,
|
||
]
|
||
|
||
def test_cannot_edit_after_submit(self):
|
||
"""Тест: нельзя редактировать после отправки."""
|
||
request = ProcurementRequest.objects.create(
|
||
title="Отправленная заявка",
|
||
description="Уже на согласовании",
|
||
department=self.department,
|
||
requestor=self.requestor,
|
||
status=ProcurementStatus.PENDING,
|
||
urgency=UrgencyLevel.MEDIUM,
|
||
)
|
||
|
||
assert request.is_editable is False
|
||
|
||
self.client.force_authenticate(user=self.requestor)
|
||
detail_url = reverse(
|
||
"api:v1:procurement:procurementrequest-detail",
|
||
kwargs={"pk": request.id},
|
||
)
|
||
|
||
response = self.client.patch(
|
||
detail_url,
|
||
{"title": "Новое название"},
|
||
)
|
||
|
||
# Должна быть ошибка прав доступа
|
||
assert response.status_code == status.HTTP_403_FORBIDDEN
|