Corp/backend/tests/procurement/test_workflow.py
2026-09-07 19:43:48 +03:00

441 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Тесты для 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