Allow procurement executor reassignment
This commit is contained in:
parent
91c9820109
commit
76799c453e
@ -129,6 +129,12 @@ class CanManageProcurementRequest(permissions.BasePermission):
|
||||
perm = perm_map.get(action)
|
||||
return perm and user.has_perm(perm)
|
||||
|
||||
def _has_procurement_execute_perm(self, user) -> bool:
|
||||
return (
|
||||
user.has_perm("procurement.change_procurementrequest")
|
||||
or user.has_perm("procurement.execute_procurement")
|
||||
)
|
||||
|
||||
def _is_dept_head(self, user, dept_id: int) -> bool:
|
||||
"""Проверяет, является ли пользователь начальником отдела."""
|
||||
return Department.objects.filter(id=dept_id, head_id=user.id).exists()
|
||||
@ -211,6 +217,8 @@ class CanManageProcurementRequest(permissions.BasePermission):
|
||||
|
||||
# Start_work: общий пул может взять любой, адресную заявку - отдел.
|
||||
if action == "start_work":
|
||||
if self._has_procurement_execute_perm(user):
|
||||
return True
|
||||
if obj.processing_department_id:
|
||||
return self._is_dept_member(
|
||||
user,
|
||||
|
||||
@ -28,6 +28,7 @@ from procurement.models import (
|
||||
Supplier,
|
||||
)
|
||||
from procurement.notifications.handlers import (
|
||||
notify_executor_reassigned,
|
||||
notify_item_comment,
|
||||
notify_item_issue_reported,
|
||||
notify_item_updated,
|
||||
@ -192,11 +193,18 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
queryset = queryset.filter(executor=user)
|
||||
elif scope == "available":
|
||||
queryset = queryset.filter(
|
||||
status__in=[
|
||||
ProcurementStatus.WAITING,
|
||||
ProcurementStatus.APPROVED,
|
||||
],
|
||||
executor__isnull=True,
|
||||
Q(
|
||||
status__in=[
|
||||
ProcurementStatus.WAITING,
|
||||
ProcurementStatus.APPROVED,
|
||||
],
|
||||
executor__isnull=True,
|
||||
)
|
||||
| Q(
|
||||
status=ProcurementStatus.IN_PROGRESS,
|
||||
processing_department__isnull=False,
|
||||
executor__isnull=False,
|
||||
)
|
||||
)
|
||||
if not (
|
||||
user.is_superuser
|
||||
@ -213,6 +221,18 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
status=ProcurementStatus.APPROVED,
|
||||
processing_department__isnull=True,
|
||||
)
|
||||
| (
|
||||
Q(
|
||||
status=ProcurementStatus.IN_PROGRESS,
|
||||
processing_department_id__in=participant_department_ids,
|
||||
)
|
||||
& ~Q(executor=user)
|
||||
)
|
||||
)
|
||||
else:
|
||||
queryset = queryset.exclude(
|
||||
status=ProcurementStatus.IN_PROGRESS,
|
||||
executor=user,
|
||||
)
|
||||
elif scope == "all":
|
||||
# Все заявки - применяем стандартную логику прав
|
||||
@ -227,6 +247,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
user.has_perm("procurement.view_procurementrequest")
|
||||
or user.has_perm("procurement.change_procurementrequest")
|
||||
or user.has_perm("procurement.delete_procurementrequest")
|
||||
or user.has_perm("procurement.execute_procurement")
|
||||
):
|
||||
pass
|
||||
else:
|
||||
@ -639,11 +660,18 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
"""Получить заявки, доступные для взятия в работу."""
|
||||
queryset = self.filter_queryset(
|
||||
self.get_queryset().filter(
|
||||
status__in=[
|
||||
ProcurementStatus.WAITING,
|
||||
ProcurementStatus.APPROVED,
|
||||
],
|
||||
executor__isnull=True,
|
||||
Q(
|
||||
status__in=[
|
||||
ProcurementStatus.WAITING,
|
||||
ProcurementStatus.APPROVED,
|
||||
],
|
||||
executor__isnull=True,
|
||||
)
|
||||
| Q(
|
||||
status=ProcurementStatus.IN_PROGRESS,
|
||||
processing_department__isnull=False,
|
||||
executor__isnull=False,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@ -667,6 +695,18 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
status=ProcurementStatus.APPROVED,
|
||||
processing_department__isnull=True,
|
||||
)
|
||||
| (
|
||||
Q(
|
||||
status=ProcurementStatus.IN_PROGRESS,
|
||||
processing_department_id__in=participant_department_ids,
|
||||
)
|
||||
& ~Q(executor=request.user)
|
||||
)
|
||||
)
|
||||
else:
|
||||
queryset = queryset.exclude(
|
||||
status=ProcurementStatus.IN_PROGRESS,
|
||||
executor=request.user,
|
||||
)
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
@ -683,15 +723,52 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
|
||||
Общий пул может взять любой авторизованный пользователь.
|
||||
Адресную заявку может взять сотрудник отдела-исполнителя.
|
||||
Адресную заявку в работе может забрать другой сотрудник отдела.
|
||||
"""
|
||||
procurement_request = self.get_object()
|
||||
self.check_object_permissions(request, procurement_request)
|
||||
|
||||
# Проверяем текущий статус
|
||||
if procurement_request.status not in [
|
||||
is_reassignment = (
|
||||
procurement_request.status == ProcurementStatus.IN_PROGRESS
|
||||
and procurement_request.processing_department_id
|
||||
and procurement_request.executor_id
|
||||
)
|
||||
|
||||
if is_reassignment:
|
||||
previous_executor = procurement_request.executor
|
||||
if previous_executor == request.user:
|
||||
return Response(
|
||||
{"error": "Вы уже являетесь исполнителем заявки"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not ProcurementApprovalResolver.user_can_start_work(
|
||||
request.user,
|
||||
procurement_request,
|
||||
):
|
||||
return Response(
|
||||
{"error": "Вы не можете забрать эту заявку в работу"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
procurement_request.executor = request.user
|
||||
procurement_request.started_at = timezone.now()
|
||||
procurement_request.save(
|
||||
update_fields=["executor", "started_at", "updated_at"],
|
||||
)
|
||||
notify_executor_reassigned(
|
||||
procurement_request,
|
||||
previous_executor,
|
||||
request.user,
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(procurement_request)
|
||||
return Response(serializer.data)
|
||||
|
||||
if procurement_request.status not in {
|
||||
ProcurementStatus.APPROVED,
|
||||
ProcurementStatus.WAITING,
|
||||
]:
|
||||
}:
|
||||
return Response(
|
||||
{
|
||||
"error": (
|
||||
@ -714,6 +791,15 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not ProcurementApprovalResolver.user_can_start_work(
|
||||
request.user,
|
||||
procurement_request,
|
||||
):
|
||||
return Response(
|
||||
{"error": "Вы не можете взять эту заявку в работу"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# Назначаем исполнителя и меняем статус
|
||||
# Сигнал post_save(ProcurementRequest) с IN_PROGRESS отправит
|
||||
# уведомления
|
||||
|
||||
@ -36,6 +36,7 @@ from .handlers import (
|
||||
notify_request_rejected,
|
||||
notify_request_completed,
|
||||
notify_request_in_progress,
|
||||
notify_executor_reassigned,
|
||||
notify_request_returned_to_processing,
|
||||
notify_request_cancelled,
|
||||
notify_stage_approved,
|
||||
@ -66,6 +67,7 @@ __all__ = [
|
||||
'notify_request_rejected',
|
||||
'notify_request_completed',
|
||||
'notify_request_in_progress',
|
||||
'notify_executor_reassigned',
|
||||
'notify_request_returned_to_processing',
|
||||
'notify_request_cancelled',
|
||||
'notify_stage_approved',
|
||||
|
||||
@ -22,6 +22,7 @@ class NotificationVerbs:
|
||||
APPROVED = 'procurement_approved'
|
||||
REJECTED = 'procurement_rejected'
|
||||
IN_PROGRESS = 'procurement_in_progress'
|
||||
EXECUTOR_REASSIGNED = 'procurement_executor_reassigned'
|
||||
COMPLETED = 'procurement_completed'
|
||||
CANCELLED = 'procurement_cancelled'
|
||||
|
||||
@ -141,6 +142,16 @@ class MessageTemplates:
|
||||
)
|
||||
return notification_title, description
|
||||
|
||||
@staticmethod
|
||||
def executor_reassigned(
|
||||
title: str, executor_name: str
|
||||
) -> tuple[str, str]:
|
||||
notification_title = 'Заявку забрал другой сотрудник'
|
||||
description = (
|
||||
f'{executor_name} взял в работу заявку "{title}".'
|
||||
)
|
||||
return notification_title, description
|
||||
|
||||
@staticmethod
|
||||
def in_progress(
|
||||
title: str, executor_name: str
|
||||
|
||||
@ -420,6 +420,36 @@ def notify_request_in_progress(request, executor):
|
||||
)
|
||||
|
||||
|
||||
def notify_executor_reassigned(request, previous_executor, actor):
|
||||
"""Уведомить прежнего исполнителя о перехвате адресной заявки."""
|
||||
if not previous_executor:
|
||||
return
|
||||
if actor and previous_executor.id == actor.id:
|
||||
return
|
||||
|
||||
notification_title, description = MessageTemplates.executor_reassigned(
|
||||
request.title,
|
||||
_actor_name(actor),
|
||||
)
|
||||
_send_procurement_notification(
|
||||
recipient=previous_executor,
|
||||
verb=NotificationVerbs.EXECUTOR_REASSIGNED,
|
||||
procurement_request=request,
|
||||
title=notification_title,
|
||||
description=description,
|
||||
actor=actor,
|
||||
extra_data={
|
||||
"old_executor_id": previous_executor.id,
|
||||
"new_executor_id": actor.id if actor else None,
|
||||
},
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[Procurement] Исполнитель заявки #{request.id} изменён: "
|
||||
f"{previous_executor.id} -> {_actor_id(actor)}"
|
||||
)
|
||||
|
||||
|
||||
def notify_request_cancelled(request):
|
||||
"""
|
||||
Уведомить об отмене заявки.
|
||||
|
||||
@ -212,19 +212,37 @@ class ProcurementApprovalResolver:
|
||||
if not user or not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if (
|
||||
procurement_request.status
|
||||
not in {ProcurementStatus.APPROVED, ProcurementStatus.WAITING}
|
||||
or procurement_request.executor_id
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
has_execute_access = (
|
||||
user.is_superuser
|
||||
or user.is_staff
|
||||
or user.has_perm("procurement.change_procurementrequest")
|
||||
or user.has_perm("procurement.execute_procurement")
|
||||
)
|
||||
|
||||
if (
|
||||
procurement_request.status == ProcurementStatus.IN_PROGRESS
|
||||
and procurement_request.processing_department_id
|
||||
and procurement_request.executor_id
|
||||
):
|
||||
if procurement_request.executor_id == user.id:
|
||||
return False
|
||||
if has_execute_access:
|
||||
return True
|
||||
return cls.user_is_processing_department_member(
|
||||
user,
|
||||
procurement_request,
|
||||
)
|
||||
|
||||
if procurement_request.status not in {
|
||||
ProcurementStatus.APPROVED,
|
||||
ProcurementStatus.WAITING,
|
||||
}:
|
||||
return False
|
||||
|
||||
if procurement_request.executor_id:
|
||||
return False
|
||||
|
||||
if has_execute_access:
|
||||
return True
|
||||
|
||||
if procurement_request.processing_department_id:
|
||||
|
||||
@ -768,6 +768,42 @@ class TestProcessingDepartmentWorkflow:
|
||||
)
|
||||
return employee
|
||||
|
||||
@pytest.fixture
|
||||
def supply_replacement_user(self, db, supply_department):
|
||||
employee = Employee.objects.create_user(
|
||||
email="supply-replacement@example.com",
|
||||
password="testpass123",
|
||||
phone_number="+79998888893",
|
||||
first_name="Новый",
|
||||
last_name="Исполнитель",
|
||||
is_active=True,
|
||||
email_verified=True,
|
||||
send_activation_email=False,
|
||||
)
|
||||
EmployeeDepartment.objects.create(
|
||||
employee=employee,
|
||||
department=supply_department,
|
||||
is_active=True,
|
||||
)
|
||||
return employee
|
||||
|
||||
@pytest.fixture
|
||||
def procurement_execute_user(self, db):
|
||||
employee = Employee.objects.create_user(
|
||||
email="procurement-execute@example.com",
|
||||
password="testpass123",
|
||||
phone_number="+79998888894",
|
||||
first_name="Права",
|
||||
last_name="Закупок",
|
||||
is_active=True,
|
||||
email_verified=True,
|
||||
send_activation_email=False,
|
||||
)
|
||||
employee.user_permissions.add(
|
||||
Permission.objects.get(codename="execute_procurement")
|
||||
)
|
||||
return employee
|
||||
|
||||
@pytest.fixture
|
||||
def outsider(self, db):
|
||||
return Employee.objects.create_user(
|
||||
@ -1053,6 +1089,42 @@ class TestProcessingDepartmentWorkflow:
|
||||
is True
|
||||
)
|
||||
|
||||
def test_processing_permission_flags_allow_reassignment_for_other_member(
|
||||
self, api_client, user, supply_user, supply_replacement_user,
|
||||
processing_request, procurement_item_factory
|
||||
):
|
||||
procurement_item_factory(request=processing_request)
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.executor = supply_user
|
||||
processing_request.started_at = timezone.now()
|
||||
processing_request.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
"executor",
|
||||
"started_at",
|
||||
"updated_at",
|
||||
],
|
||||
)
|
||||
detail_url = reverse(
|
||||
'api:v1:procurement:procurementrequest-detail',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
executor_response = api_client.get(detail_url)
|
||||
assert executor_response.status_code == status.HTTP_200_OK
|
||||
assert executor_response.data["can_current_user_start_work"] is False
|
||||
|
||||
api_client.force_authenticate(user=supply_replacement_user)
|
||||
replacement_response = api_client.get(detail_url)
|
||||
assert replacement_response.status_code == status.HTTP_200_OK
|
||||
assert replacement_response.data["can_current_user_start_work"] is True
|
||||
|
||||
api_client.force_authenticate(user=user)
|
||||
author_response = api_client.get(detail_url)
|
||||
assert author_response.status_code == status.HTTP_200_OK
|
||||
assert author_response.data["can_current_user_start_work"] is False
|
||||
|
||||
def test_outsider_cannot_start_processing_department_request(
|
||||
self, api_client, outsider, processing_request, procurement_item_factory
|
||||
):
|
||||
@ -1118,6 +1190,264 @@ class TestProcessingDepartmentWorkflow:
|
||||
assert processing_request.executor == supply_role_user
|
||||
assert processing_request.status == ProcurementStatus.IN_PROGRESS
|
||||
|
||||
def test_processing_department_user_can_reassign_in_progress_request(
|
||||
self, api_client, supply_user, supply_replacement_user,
|
||||
processing_request, procurement_item_factory
|
||||
):
|
||||
procurement_item_factory(request=processing_request)
|
||||
previous_started_at = timezone.now()
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.executor = supply_user
|
||||
processing_request.started_at = previous_started_at
|
||||
processing_request.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
"executor",
|
||||
"started_at",
|
||||
"updated_at",
|
||||
],
|
||||
)
|
||||
api_client.force_authenticate(user=supply_replacement_user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:procurementrequest-start-work',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
processing_request.refresh_from_db()
|
||||
assert processing_request.executor == supply_replacement_user
|
||||
assert processing_request.status == ProcurementStatus.IN_PROGRESS
|
||||
assert processing_request.started_at > previous_started_at
|
||||
assert response.data["executor"] == supply_replacement_user.id
|
||||
assert response.data["executor_name"] == (
|
||||
supply_replacement_user.get_full_name()
|
||||
)
|
||||
|
||||
def test_available_scope_includes_reassignable_processing_request(
|
||||
self, api_client, supply_user, supply_replacement_user,
|
||||
processing_request, procurement_item_factory
|
||||
):
|
||||
procurement_item_factory(request=processing_request)
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.executor = supply_user
|
||||
processing_request.started_at = timezone.now()
|
||||
processing_request.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
"executor",
|
||||
"started_at",
|
||||
"updated_at",
|
||||
],
|
||||
)
|
||||
api_client.force_authenticate(user=supply_replacement_user)
|
||||
url = reverse('api:v1:procurement:procurementrequest-list')
|
||||
|
||||
response = api_client.get(f"{url}?scope=available")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
ids = [item['id'] for item in response.data['results']]
|
||||
assert processing_request.id in ids
|
||||
|
||||
def test_procurement_execute_permission_can_reassign_addressed_request(
|
||||
self, api_client, supply_user, procurement_execute_user,
|
||||
processing_request, procurement_item_factory
|
||||
):
|
||||
procurement_item_factory(request=processing_request)
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.executor = supply_user
|
||||
processing_request.started_at = timezone.now()
|
||||
processing_request.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
"executor",
|
||||
"started_at",
|
||||
"updated_at",
|
||||
],
|
||||
)
|
||||
api_client.force_authenticate(user=procurement_execute_user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:procurementrequest-start-work',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
processing_request.refresh_from_db()
|
||||
assert processing_request.executor == procurement_execute_user
|
||||
|
||||
def test_processing_department_role_user_can_reassign_in_progress_request(
|
||||
self, api_client, supply_user, supply_role_user,
|
||||
processing_request, procurement_item_factory
|
||||
):
|
||||
procurement_item_factory(request=processing_request)
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.executor = supply_user
|
||||
processing_request.started_at = timezone.now()
|
||||
processing_request.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
"executor",
|
||||
"started_at",
|
||||
"updated_at",
|
||||
],
|
||||
)
|
||||
api_client.force_authenticate(user=supply_role_user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:procurementrequest-start-work',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
processing_request.refresh_from_db()
|
||||
assert processing_request.executor == supply_role_user
|
||||
assert processing_request.status == ProcurementStatus.IN_PROGRESS
|
||||
|
||||
def test_reassign_in_progress_request_notifies_previous_executor_only(
|
||||
self, api_client, user, supply_user, supply_replacement_user,
|
||||
processing_request, procurement_item_factory, monkeypatch
|
||||
):
|
||||
sent = []
|
||||
|
||||
def fake_notify_send(**kwargs):
|
||||
sent.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"procurement.notifications.handlers.notify.send",
|
||||
fake_notify_send,
|
||||
)
|
||||
|
||||
procurement_item_factory(request=processing_request)
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.executor = supply_user
|
||||
processing_request.started_at = timezone.now()
|
||||
processing_request.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
"executor",
|
||||
"started_at",
|
||||
"updated_at",
|
||||
],
|
||||
)
|
||||
sent.clear()
|
||||
api_client.force_authenticate(user=supply_replacement_user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:procurementrequest-start-work',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
reassignment_notifications = [
|
||||
item for item in sent
|
||||
if item["verb"] == "procurement_executor_reassigned"
|
||||
]
|
||||
assert len(reassignment_notifications) == 1
|
||||
notification = reassignment_notifications[0]
|
||||
assert notification["recipient"] == supply_user
|
||||
assert notification["sender"] == supply_replacement_user
|
||||
assert notification["data"]["title"] == (
|
||||
"Заявку забрал другой сотрудник"
|
||||
)
|
||||
assert notification["description"] == (
|
||||
f'{supply_replacement_user.get_full_name()} взял в работу заявку '
|
||||
f'"{processing_request.title}".'
|
||||
)
|
||||
assert user not in [
|
||||
item["recipient"] for item in reassignment_notifications
|
||||
]
|
||||
assert "procurement_in_progress" not in [
|
||||
item["verb"] for item in sent
|
||||
]
|
||||
|
||||
def test_current_executor_cannot_reassign_same_request(
|
||||
self, api_client, supply_user, processing_request,
|
||||
procurement_item_factory
|
||||
):
|
||||
procurement_item_factory(request=processing_request)
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.executor = supply_user
|
||||
processing_request.started_at = timezone.now()
|
||||
processing_request.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
"executor",
|
||||
"started_at",
|
||||
"updated_at",
|
||||
],
|
||||
)
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:procurementrequest-start-work',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
processing_request.refresh_from_db()
|
||||
assert processing_request.executor == supply_user
|
||||
|
||||
def test_outsider_cannot_reassign_in_progress_request(
|
||||
self, api_client, outsider, supply_user, processing_request,
|
||||
procurement_item_factory
|
||||
):
|
||||
procurement_item_factory(request=processing_request)
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.executor = supply_user
|
||||
processing_request.started_at = timezone.now()
|
||||
processing_request.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
"executor",
|
||||
"started_at",
|
||||
"updated_at",
|
||||
],
|
||||
)
|
||||
api_client.force_authenticate(user=outsider)
|
||||
url = reverse(
|
||||
'api:v1:procurement:procurementrequest-start-work',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
processing_request.refresh_from_db()
|
||||
assert processing_request.executor == supply_user
|
||||
|
||||
def test_non_processing_in_progress_request_cannot_be_reassigned(
|
||||
self, api_client, user, staff_user, procurement_request,
|
||||
procurement_item
|
||||
):
|
||||
procurement_request.status = ProcurementStatus.IN_PROGRESS
|
||||
procurement_request.executor = staff_user
|
||||
procurement_request.started_at = timezone.now()
|
||||
procurement_request.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
"executor",
|
||||
"started_at",
|
||||
"updated_at",
|
||||
],
|
||||
)
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:procurementrequest-start-work',
|
||||
kwargs={'pk': procurement_request.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
procurement_request.refresh_from_db()
|
||||
assert procurement_request.executor == staff_user
|
||||
|
||||
def test_processing_department_role_user_can_manage_items(
|
||||
self, api_client, supply_role_user, processing_request,
|
||||
procurement_item_factory
|
||||
|
||||
@ -191,6 +191,7 @@ function ProcurementRequestActionButtons({
|
||||
const isPending = status === "pending";
|
||||
const canStartWork = Boolean(request.can_current_user_start_work);
|
||||
const isInProgress = status === "in_progress";
|
||||
const startWorkLabel = isInProgress ? "Забрать в работу" : "Взять в работу";
|
||||
const canApproveThis = Boolean(request.can_current_user_approve);
|
||||
const canSubmitForApproval = Boolean(request.can_current_user_submit_for_approval);
|
||||
const buttonClass = (variantClass: string) =>
|
||||
@ -266,11 +267,11 @@ function ProcurementRequestActionButtons({
|
||||
type="button"
|
||||
onClick={() => onStart(request.id)}
|
||||
disabled={busyKey === `start-${request.id}`}
|
||||
title="Взять в работу"
|
||||
title={startWorkLabel}
|
||||
className={buttonClass("app-action-primary")}
|
||||
>
|
||||
<Play size={14} />
|
||||
{label("Взять в работу")}
|
||||
{label(startWorkLabel)}
|
||||
</button>
|
||||
) : null}
|
||||
{isInProgress && isExecutor ? (
|
||||
|
||||
@ -44,6 +44,7 @@ export const VERB_CATEGORIES: Record<string, string> = {
|
||||
procurement_approved: 'Закупки',
|
||||
procurement_rejected: 'Закупки',
|
||||
procurement_in_progress: 'Закупки',
|
||||
procurement_executor_reassigned: 'Закупки',
|
||||
procurement_completed: 'Закупки',
|
||||
procurement_cancelled: 'Закупки',
|
||||
procurement_item_updated: 'Закупки',
|
||||
@ -99,6 +100,7 @@ export const VERB_NAMES: Record<string, string> = {
|
||||
procurement_approved: 'Закупка одобрена',
|
||||
procurement_rejected: 'Закупка отклонена',
|
||||
procurement_in_progress: 'Закупка взята в работу',
|
||||
procurement_executor_reassigned: 'Заявку забрал другой сотрудник',
|
||||
procurement_completed: 'Закупка завершена',
|
||||
procurement_cancelled: 'Закупка отменена',
|
||||
procurement_item_updated: 'Позиция закупки изменена',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user