diff --git a/backend/api/v1/procurement/permissions.py b/backend/api/v1/procurement/permissions.py
index 6dc0291c..96836c55 100644
--- a/backend/api/v1/procurement/permissions.py
+++ b/backend/api/v1/procurement/permissions.py
@@ -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,
diff --git a/backend/api/v1/procurement/views.py b/backend/api/v1/procurement/views.py
index 0b964019..d71b458c 100644
--- a/backend/api/v1/procurement/views.py
+++ b/backend/api/v1/procurement/views.py
@@ -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 отправит
# уведомления
diff --git a/backend/procurement/notifications/__init__.py b/backend/procurement/notifications/__init__.py
index 7ad83d91..d63a5d02 100644
--- a/backend/procurement/notifications/__init__.py
+++ b/backend/procurement/notifications/__init__.py
@@ -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',
diff --git a/backend/procurement/notifications/config.py b/backend/procurement/notifications/config.py
index 88445a30..92eb3503 100644
--- a/backend/procurement/notifications/config.py
+++ b/backend/procurement/notifications/config.py
@@ -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
diff --git a/backend/procurement/notifications/handlers.py b/backend/procurement/notifications/handlers.py
index 213032a2..b99edc78 100644
--- a/backend/procurement/notifications/handlers.py
+++ b/backend/procurement/notifications/handlers.py
@@ -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):
"""
Уведомить об отмене заявки.
diff --git a/backend/procurement/services.py b/backend/procurement/services.py
index 3a2d69c4..338f9f24 100644
--- a/backend/procurement/services.py
+++ b/backend/procurement/services.py
@@ -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:
diff --git a/backend/tests/api/v1/procurement/test_procurement_requests.py b/backend/tests/api/v1/procurement/test_procurement_requests.py
index 93ff9e10..3362886a 100644
--- a/backend/tests/api/v1/procurement/test_procurement_requests.py
+++ b/backend/tests/api/v1/procurement/test_procurement_requests.py
@@ -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
diff --git a/frontend/src/app/procurement/page.tsx b/frontend/src/app/procurement/page.tsx
index 1587dce8..f08432f6 100644
--- a/frontend/src/app/procurement/page.tsx
+++ b/frontend/src/app/procurement/page.tsx
@@ -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")}
>
- {label("Взять в работу")}
+ {label(startWorkLabel)}
) : null}
{isInProgress && isExecutor ? (
diff --git a/frontend/src/lib/verbTranslations.ts b/frontend/src/lib/verbTranslations.ts
index 794d79ed..963680f6 100644
--- a/frontend/src/lib/verbTranslations.ts
+++ b/frontend/src/lib/verbTranslations.ts
@@ -44,6 +44,7 @@ export const VERB_CATEGORIES: Record = {
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 = {
procurement_approved: 'Закупка одобрена',
procurement_rejected: 'Закупка отклонена',
procurement_in_progress: 'Закупка взята в работу',
+ procurement_executor_reassigned: 'Заявку забрал другой сотрудник',
procurement_completed: 'Закупка завершена',
procurement_cancelled: 'Закупка отменена',
procurement_item_updated: 'Позиция закупки изменена',