procurement: add processing department workflow
This commit is contained in:
parent
3725975604
commit
fce272d352
@ -153,6 +153,17 @@ class CanManageProcurementRequest(permissions.BasePermission):
|
||||
"""Проверяет, является ли пользователь начальником отдела."""
|
||||
return Department.objects.filter(id=dept_id, head_id=user.id).exists()
|
||||
|
||||
def _is_dept_member(self, user, dept_id: int) -> bool:
|
||||
"""Проверяет активную принадлежность пользователя к отделу."""
|
||||
return (
|
||||
EmployeeDepartment.objects.filter(
|
||||
employee_id=user.id,
|
||||
department_id=dept_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
or self._is_dept_head(user, dept_id)
|
||||
)
|
||||
|
||||
def has_permission(self, request, view) -> bool:
|
||||
"""Проверка доступа на уровне запроса."""
|
||||
user = request.user
|
||||
@ -235,14 +246,29 @@ class CanManageProcurementRequest(permissions.BasePermission):
|
||||
if action == "delete_comment":
|
||||
return True
|
||||
|
||||
# Start_work (взять в работу) - любой авторизованный
|
||||
# Start_work: общий пул может взять любой, адресную заявку - отдел.
|
||||
if action == "start_work":
|
||||
if obj.processing_department_id:
|
||||
return self._is_dept_member(
|
||||
user,
|
||||
obj.processing_department_id,
|
||||
)
|
||||
return True
|
||||
|
||||
# Complete (завершить) - только исполнитель
|
||||
if action == "complete":
|
||||
return obj.executor == user
|
||||
|
||||
if action == "mark_all_received":
|
||||
if obj.executor == user:
|
||||
return True
|
||||
if obj.processing_department_id:
|
||||
return self._is_dept_member(
|
||||
user,
|
||||
obj.processing_department_id,
|
||||
)
|
||||
return False
|
||||
|
||||
# Удаление
|
||||
if action == "destroy":
|
||||
# Автор может удалить свою заявку в DRAFT
|
||||
@ -255,6 +281,12 @@ class CanManageProcurementRequest(permissions.BasePermission):
|
||||
|
||||
# Изменение
|
||||
if action in ("update", "partial_update"):
|
||||
if obj.processing_department_id and self._is_dept_member(
|
||||
user,
|
||||
obj.processing_department_id,
|
||||
):
|
||||
return True
|
||||
|
||||
# Заявка должна быть редактируемой (DRAFT)
|
||||
if not obj.is_editable:
|
||||
return False
|
||||
|
||||
@ -5,7 +5,10 @@
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from procurement.constants import get_default_approval_step_name
|
||||
from procurement.constants import (
|
||||
ProcurementStatus,
|
||||
get_default_approval_step_name,
|
||||
)
|
||||
from procurement.services import ProcurementApprovalResolver
|
||||
from procurement.models import (
|
||||
Approval,
|
||||
@ -26,6 +29,9 @@ class ProcurementItemSerializer(serializers.ModelSerializer):
|
||||
total_price = serializers.DecimalField(
|
||||
max_digits=12, decimal_places=2, read_only=True
|
||||
)
|
||||
execution_status_display = serializers.CharField(
|
||||
source="get_execution_status_display", read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProcurementItem
|
||||
@ -39,10 +45,21 @@ class ProcurementItemSerializer(serializers.ModelSerializer):
|
||||
"estimated_unit_price",
|
||||
"total_price",
|
||||
"supplier_info",
|
||||
"links",
|
||||
"expected_delivery_date",
|
||||
"actual_unit_price",
|
||||
"execution_status",
|
||||
"execution_status_display",
|
||||
"executor_comment",
|
||||
"equipment",
|
||||
"created_at",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "total_price"]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"created_at",
|
||||
"total_price",
|
||||
"execution_status_display",
|
||||
]
|
||||
|
||||
|
||||
class ProcurementItemCreateSerializer(serializers.ModelSerializer):
|
||||
@ -57,6 +74,11 @@ class ProcurementItemCreateSerializer(serializers.ModelSerializer):
|
||||
"unit",
|
||||
"estimated_unit_price",
|
||||
"supplier_info",
|
||||
"links",
|
||||
"expected_delivery_date",
|
||||
"actual_unit_price",
|
||||
"execution_status",
|
||||
"executor_comment",
|
||||
]
|
||||
|
||||
|
||||
@ -109,6 +131,9 @@ class ProcurementRequestListSerializer(serializers.ModelSerializer):
|
||||
department_name = serializers.CharField(
|
||||
source="department.name", read_only=True
|
||||
)
|
||||
processing_department_name = serializers.CharField(
|
||||
source="processing_department.name", read_only=True, allow_null=True
|
||||
)
|
||||
requestor_name = serializers.CharField(
|
||||
source="requestor.get_full_name", read_only=True
|
||||
)
|
||||
@ -118,6 +143,9 @@ class ProcurementRequestListSerializer(serializers.ModelSerializer):
|
||||
urgency_display = serializers.CharField(
|
||||
source="get_urgency_display", read_only=True
|
||||
)
|
||||
fulfillment_status_display = serializers.CharField(
|
||||
source="get_fulfillment_status_display", read_only=True
|
||||
)
|
||||
items_count = serializers.IntegerField(read_only=True)
|
||||
total_cost = serializers.DecimalField(
|
||||
max_digits=12, decimal_places=2, read_only=True
|
||||
@ -141,6 +169,8 @@ class ProcurementRequestListSerializer(serializers.ModelSerializer):
|
||||
"title",
|
||||
"department",
|
||||
"department_name",
|
||||
"processing_department",
|
||||
"processing_department_name",
|
||||
"requestor",
|
||||
"requestor_name",
|
||||
"executor",
|
||||
@ -149,6 +179,8 @@ class ProcurementRequestListSerializer(serializers.ModelSerializer):
|
||||
"status_display",
|
||||
"urgency",
|
||||
"urgency_display",
|
||||
"fulfillment_status",
|
||||
"fulfillment_status_display",
|
||||
"total_cost",
|
||||
"items_count",
|
||||
"comments_count",
|
||||
@ -163,10 +195,13 @@ class ProcurementRequestListSerializer(serializers.ModelSerializer):
|
||||
"submitted_at",
|
||||
"started_at",
|
||||
"department_name",
|
||||
"processing_department_name",
|
||||
"requestor_name",
|
||||
"executor_name",
|
||||
"status_display",
|
||||
"urgency_display",
|
||||
"fulfillment_status",
|
||||
"fulfillment_status_display",
|
||||
"items_count",
|
||||
"total_cost",
|
||||
"comments_count",
|
||||
@ -191,6 +226,9 @@ class ProcurementRequestDetailSerializer(serializers.ModelSerializer):
|
||||
department_name = serializers.CharField(
|
||||
source="department.name", read_only=True
|
||||
)
|
||||
processing_department_name = serializers.CharField(
|
||||
source="processing_department.name", read_only=True, allow_null=True
|
||||
)
|
||||
requestor_name = serializers.CharField(
|
||||
source="requestor.get_full_name", read_only=True
|
||||
)
|
||||
@ -206,6 +244,9 @@ class ProcurementRequestDetailSerializer(serializers.ModelSerializer):
|
||||
urgency_display = serializers.CharField(
|
||||
source="get_urgency_display", read_only=True
|
||||
)
|
||||
fulfillment_status_display = serializers.CharField(
|
||||
source="get_fulfillment_status_display", read_only=True
|
||||
)
|
||||
total_cost = serializers.DecimalField(
|
||||
max_digits=12, decimal_places=2, read_only=True
|
||||
)
|
||||
@ -232,6 +273,8 @@ class ProcurementRequestDetailSerializer(serializers.ModelSerializer):
|
||||
"description",
|
||||
"department",
|
||||
"department_name",
|
||||
"processing_department",
|
||||
"processing_department_name",
|
||||
"requestor",
|
||||
"requestor_name",
|
||||
"requestor_email",
|
||||
@ -241,6 +284,8 @@ class ProcurementRequestDetailSerializer(serializers.ModelSerializer):
|
||||
"status_display",
|
||||
"urgency",
|
||||
"urgency_display",
|
||||
"fulfillment_status",
|
||||
"fulfillment_status_display",
|
||||
"total_cost",
|
||||
"actual_cost",
|
||||
"items",
|
||||
@ -265,6 +310,9 @@ class ProcurementRequestDetailSerializer(serializers.ModelSerializer):
|
||||
"is_editable",
|
||||
"total_cost",
|
||||
"executor_name",
|
||||
"fulfillment_status",
|
||||
"processing_department_name",
|
||||
"fulfillment_status_display",
|
||||
"comments_count",
|
||||
"can_current_user_approve",
|
||||
]
|
||||
@ -282,6 +330,7 @@ class ProcurementRequestCreateSerializer(serializers.ModelSerializer):
|
||||
"title",
|
||||
"description",
|
||||
"department",
|
||||
"processing_department",
|
||||
"requestor",
|
||||
"urgency",
|
||||
"items",
|
||||
@ -295,7 +344,9 @@ class ProcurementRequestCreateSerializer(serializers.ModelSerializer):
|
||||
items_data = validated_data.pop("items", [])
|
||||
request = self.context["request"]
|
||||
|
||||
# Создаем заявку
|
||||
if validated_data.get("processing_department"):
|
||||
validated_data["status"] = ProcurementStatus.WAITING
|
||||
|
||||
procurement_request = ProcurementRequest.objects.create(
|
||||
requestor=request.user, **validated_data
|
||||
)
|
||||
|
||||
@ -8,11 +8,12 @@ from rest_framework import filters, permissions, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from employees.models import Department
|
||||
from employees.models import Department, EmployeeDepartment
|
||||
from procurement.constants import (
|
||||
ApprovalStatus,
|
||||
ProcurementStatus,
|
||||
EquipmentStatus,
|
||||
ProcurementItemExecutionStatus,
|
||||
)
|
||||
from procurement.models import (
|
||||
Approval,
|
||||
@ -55,14 +56,24 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet для заявок на закупку."""
|
||||
|
||||
queryset = ProcurementRequest.objects.select_related(
|
||||
"department", "requestor", "executor"
|
||||
"department",
|
||||
"processing_department",
|
||||
"requestor",
|
||||
"executor",
|
||||
).prefetch_related("items", "approvals__approver")
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
filters.SearchFilter,
|
||||
filters.OrderingFilter,
|
||||
]
|
||||
filterset_fields = ["status", "urgency", "department", "executor"]
|
||||
filterset_fields = [
|
||||
"status",
|
||||
"urgency",
|
||||
"department",
|
||||
"processing_department",
|
||||
"executor",
|
||||
"fulfillment_status",
|
||||
]
|
||||
search_fields = ["title", "description"]
|
||||
ordering_fields = ["created_at", "status"]
|
||||
ordering = ["-created_at"]
|
||||
@ -79,6 +90,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
"start_work",
|
||||
"complete",
|
||||
"cancel",
|
||||
"mark_all_received",
|
||||
]:
|
||||
return ProcurementRequestDetailSerializer
|
||||
return ProcurementRequestListSerializer
|
||||
@ -138,8 +150,29 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
# Заявки, которые я взял в работу (где я исполнитель)
|
||||
queryset = queryset.filter(executor=user)
|
||||
elif scope == "available":
|
||||
# Доступные заявки - со статусом "Согласовано"
|
||||
queryset = queryset.filter(status="approved")
|
||||
queryset = queryset.filter(
|
||||
status__in=[
|
||||
ProcurementStatus.WAITING,
|
||||
ProcurementStatus.APPROVED,
|
||||
],
|
||||
executor__isnull=True,
|
||||
)
|
||||
if not (
|
||||
user.is_superuser
|
||||
or user.is_staff
|
||||
or user.has_perm("procurement.change_procurementrequest")
|
||||
or user.has_perm("procurement.execute_procurement")
|
||||
):
|
||||
queryset = queryset.filter(
|
||||
Q(
|
||||
status=ProcurementStatus.WAITING,
|
||||
processing_department__in=user.departments.all(),
|
||||
)
|
||||
| Q(
|
||||
status=ProcurementStatus.APPROVED,
|
||||
processing_department__isnull=True,
|
||||
)
|
||||
)
|
||||
elif scope == "all":
|
||||
# Все заявки - применяем стандартную логику прав
|
||||
pass
|
||||
@ -160,7 +193,12 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
queryset = queryset.filter(
|
||||
Q(requestor=user)
|
||||
| Q(department__in=user.departments.all())
|
||||
| Q(processing_department__in=user.departments.all())
|
||||
| Q(approvals__approver=user)
|
||||
| Q(
|
||||
status=ProcurementStatus.APPROVED,
|
||||
processing_department__isnull=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Фильтрация по периоду
|
||||
@ -282,6 +320,17 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
|
||||
procurement_request = self.get_object()
|
||||
|
||||
if procurement_request.processing_department_id:
|
||||
return Response(
|
||||
{
|
||||
"error": (
|
||||
"Заявка уже направлена в отдел-исполнитель "
|
||||
"и не требует согласования"
|
||||
)
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Проверки
|
||||
if procurement_request.requestor != request.user:
|
||||
return Response(
|
||||
@ -509,13 +558,34 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@action(detail=False, methods=["get"])
|
||||
def available(self, request):
|
||||
"""Получить одобренные заявки, доступные для взятия в работу."""
|
||||
"""Получить заявки, доступные для взятия в работу."""
|
||||
queryset = self.filter_queryset(
|
||||
self.get_queryset().filter(
|
||||
status=ProcurementStatus.APPROVED, executor__isnull=True
|
||||
status__in=[
|
||||
ProcurementStatus.WAITING,
|
||||
ProcurementStatus.APPROVED,
|
||||
],
|
||||
executor__isnull=True,
|
||||
)
|
||||
)
|
||||
|
||||
if not (
|
||||
request.user.is_superuser
|
||||
or request.user.is_staff
|
||||
or request.user.has_perm("procurement.change_procurementrequest")
|
||||
or request.user.has_perm("procurement.execute_procurement")
|
||||
):
|
||||
queryset = queryset.filter(
|
||||
Q(
|
||||
status=ProcurementStatus.WAITING,
|
||||
processing_department__in=request.user.departments.all(),
|
||||
)
|
||||
| Q(
|
||||
status=ProcurementStatus.APPROVED,
|
||||
processing_department__isnull=True,
|
||||
)
|
||||
)
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = ProcurementRequestListSerializer(page, many=True)
|
||||
@ -528,15 +598,24 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
def start_work(self, request, pk=None):
|
||||
"""Начать работу над заявкой (перевод в статус IN_PROGRESS).
|
||||
|
||||
Любой авторизованный пользователь может взять заявку в работу.
|
||||
TODO: Позже добавить проверку роли "Закупщик".
|
||||
Общий пул может взять любой авторизованный пользователь.
|
||||
Адресную заявку может взять сотрудник отдела-исполнителя.
|
||||
"""
|
||||
procurement_request = self.get_object()
|
||||
self.check_object_permissions(request, procurement_request)
|
||||
|
||||
# Проверяем текущий статус
|
||||
if procurement_request.status != ProcurementStatus.APPROVED:
|
||||
if procurement_request.status not in [
|
||||
ProcurementStatus.APPROVED,
|
||||
ProcurementStatus.WAITING,
|
||||
]:
|
||||
return Response(
|
||||
{"error": "Только одобренные заявки можно взять в работу"},
|
||||
{
|
||||
"error": (
|
||||
"Только одобренные или ожидающие заявки "
|
||||
"можно взять в работу"
|
||||
)
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -592,6 +671,35 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
serializer = self.get_serializer(procurement_request)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def mark_all_received(self, request, pk=None):
|
||||
"""Отметить все позиции заявки как полученные."""
|
||||
procurement_request = self.get_object()
|
||||
self.check_object_permissions(request, procurement_request)
|
||||
|
||||
if procurement_request.status not in [
|
||||
ProcurementStatus.WAITING,
|
||||
ProcurementStatus.IN_PROGRESS,
|
||||
]:
|
||||
return Response(
|
||||
{
|
||||
"error": (
|
||||
"Отмечать позиции можно только в ожидающей "
|
||||
"или взятой в работу заявке"
|
||||
)
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
procurement_request.items.update(
|
||||
execution_status=ProcurementItemExecutionStatus.RECEIVED,
|
||||
)
|
||||
procurement_request.recalculate_fulfillment_status(save=True)
|
||||
procurement_request.refresh_from_db()
|
||||
|
||||
serializer = self.get_serializer(procurement_request)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def cancel(self, request, pk=None):
|
||||
"""Отменить заявку."""
|
||||
@ -631,12 +739,41 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
class ProcurementItemViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet для позиций заявок."""
|
||||
|
||||
queryset = ProcurementItem.objects.select_related("request")
|
||||
queryset = ProcurementItem.objects.select_related(
|
||||
"request",
|
||||
"request__department",
|
||||
"request__processing_department",
|
||||
)
|
||||
serializer_class = ProcurementItemSerializer
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||
filterset_fields = ["request"]
|
||||
search_fields = ["name", "description"]
|
||||
|
||||
def _is_processing_department_member(self, user, procurement_request):
|
||||
if not procurement_request.processing_department_id:
|
||||
return False
|
||||
return (
|
||||
EmployeeDepartment.objects.filter(
|
||||
employee_id=user.id,
|
||||
department_id=procurement_request.processing_department_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
or procurement_request.processing_department.head_id == user.id
|
||||
)
|
||||
|
||||
def _can_process_request_items(self, user, procurement_request):
|
||||
if user.is_superuser or user.is_staff:
|
||||
return True
|
||||
if (
|
||||
user.has_perm("procurement.change_procurementrequest")
|
||||
or user.has_perm("procurement.execute_procurement")
|
||||
):
|
||||
return True
|
||||
return self._is_processing_department_member(
|
||||
user,
|
||||
procurement_request,
|
||||
)
|
||||
|
||||
def get_permissions(self):
|
||||
"""Только создатель заявки может редактировать позиции."""
|
||||
if self.action in [
|
||||
@ -666,31 +803,30 @@ class ProcurementItemViewSet(viewsets.ModelViewSet):
|
||||
procurement_request = serializer.validated_data.get("request")
|
||||
user = self.request.user
|
||||
|
||||
# Проверяем, что заявка в DRAFT
|
||||
if not procurement_request.is_editable:
|
||||
if procurement_request.status in [
|
||||
ProcurementStatus.CANCELLED,
|
||||
ProcurementStatus.REJECTED,
|
||||
]:
|
||||
raise PermissionDenied(
|
||||
"Нельзя добавлять позиции в заявку со статусом '{}'".format(
|
||||
procurement_request.get_status_display()
|
||||
)
|
||||
)
|
||||
|
||||
# Админы могут редактировать
|
||||
if user.is_superuser or user.is_staff:
|
||||
serializer.save()
|
||||
return
|
||||
|
||||
# Модельные права
|
||||
if user.has_perm("procurement.change_procurementrequest"):
|
||||
if self._can_process_request_items(user, procurement_request):
|
||||
serializer.save()
|
||||
return
|
||||
|
||||
# Автор заявки
|
||||
if procurement_request.requestor == user:
|
||||
if procurement_request.requestor == user and procurement_request.is_editable:
|
||||
serializer.save()
|
||||
return
|
||||
|
||||
# Начальник отдела
|
||||
if procurement_request.department.head == user:
|
||||
if (
|
||||
procurement_request.department.head == user
|
||||
and procurement_request.is_editable
|
||||
):
|
||||
serializer.save()
|
||||
return
|
||||
|
||||
@ -707,28 +843,28 @@ class ProcurementItemViewSet(viewsets.ModelViewSet):
|
||||
user = self.request.user
|
||||
procurement_request = item.request
|
||||
|
||||
# Проверяем, что заявка в DRAFT
|
||||
if not procurement_request.is_editable:
|
||||
if procurement_request.status in [
|
||||
ProcurementStatus.CANCELLED,
|
||||
ProcurementStatus.REJECTED,
|
||||
]:
|
||||
raise PermissionDenied(
|
||||
"Нельзя изменять позиции в заявке со статусом '{}'".format(
|
||||
procurement_request.get_status_display()
|
||||
)
|
||||
)
|
||||
|
||||
# Админы могут редактировать
|
||||
if user.is_superuser or user.is_staff:
|
||||
return True
|
||||
|
||||
# Модельные права
|
||||
if user.has_perm("procurement.change_procurementrequest"):
|
||||
if self._can_process_request_items(user, procurement_request):
|
||||
return True
|
||||
|
||||
# Автор заявки
|
||||
if procurement_request.requestor == user:
|
||||
if procurement_request.requestor == user and procurement_request.is_editable:
|
||||
return True
|
||||
|
||||
# Начальник отдела
|
||||
if procurement_request.department.head == user:
|
||||
if (
|
||||
procurement_request.department.head == user
|
||||
and procurement_request.is_editable
|
||||
):
|
||||
return True
|
||||
|
||||
# Нет прав
|
||||
|
||||
@ -459,6 +459,7 @@ SPECTACULAR_SETTINGS = {
|
||||
"ENUM_NAME_OVERRIDES": {
|
||||
"ProcurementStatusEnum": [
|
||||
("draft", "Черновик"),
|
||||
("waiting", "Ожидает"),
|
||||
("pending", "На согласовании"),
|
||||
("approved", "Одобрено"),
|
||||
("rejected", "Отклонено"),
|
||||
|
||||
@ -26,6 +26,9 @@ class ProcurementItemInline(admin.TabularInline):
|
||||
"quantity",
|
||||
"unit",
|
||||
"estimated_unit_price",
|
||||
"actual_unit_price",
|
||||
"execution_status",
|
||||
"expected_delivery_date",
|
||||
"supplier_info",
|
||||
]
|
||||
|
||||
@ -35,7 +38,14 @@ class ApprovalInline(admin.TabularInline):
|
||||
|
||||
model = Approval
|
||||
extra = 0
|
||||
fields = ["approver", "role", "status", "comment", "created_at"]
|
||||
fields = [
|
||||
"approver",
|
||||
"priority",
|
||||
"step_name",
|
||||
"status",
|
||||
"comment",
|
||||
"created_at",
|
||||
]
|
||||
readonly_fields = ["created_at"]
|
||||
can_delete = False
|
||||
|
||||
@ -48,13 +58,22 @@ class ProcurementRequestAdmin(admin.ModelAdmin):
|
||||
"id",
|
||||
"title",
|
||||
"department",
|
||||
"processing_department",
|
||||
"requestor",
|
||||
"status_badge",
|
||||
"fulfillment_status",
|
||||
"urgency_badge",
|
||||
"get_total_cost",
|
||||
"created_at",
|
||||
]
|
||||
list_filter = ["status", "urgency", "department", "created_at"]
|
||||
list_filter = [
|
||||
"status",
|
||||
"fulfillment_status",
|
||||
"urgency",
|
||||
"department",
|
||||
"processing_department",
|
||||
"created_at",
|
||||
]
|
||||
search_fields = ["title", "description", "requestor__username"]
|
||||
readonly_fields = [
|
||||
"created_at",
|
||||
@ -69,13 +88,22 @@ class ProcurementRequestAdmin(admin.ModelAdmin):
|
||||
fieldsets = (
|
||||
(
|
||||
"Основная информация",
|
||||
{"fields": ("title", "description", "department", "requestor")},
|
||||
{
|
||||
"fields": (
|
||||
"title",
|
||||
"description",
|
||||
"department",
|
||||
"processing_department",
|
||||
"requestor",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Параметры",
|
||||
{
|
||||
"fields": (
|
||||
"status",
|
||||
"fulfillment_status",
|
||||
"urgency",
|
||||
"get_total_cost",
|
||||
"actual_cost",
|
||||
@ -100,6 +128,7 @@ class ProcurementRequestAdmin(admin.ModelAdmin):
|
||||
"""Цветной бадж статуса."""
|
||||
colors = {
|
||||
"draft": "#6c757d",
|
||||
"waiting": "#ffc107",
|
||||
"pending": "#0dcaf0",
|
||||
"approved": "#198754",
|
||||
"rejected": "#dc3545",
|
||||
@ -153,9 +182,12 @@ class ProcurementItemAdmin(admin.ModelAdmin):
|
||||
"quantity",
|
||||
"unit",
|
||||
"estimated_unit_price",
|
||||
"actual_unit_price",
|
||||
"execution_status",
|
||||
"expected_delivery_date",
|
||||
"total_price",
|
||||
]
|
||||
list_filter = ["unit", "created_at"]
|
||||
list_filter = ["unit", "execution_status", "created_at"]
|
||||
search_fields = ["name", "description", "request__title"]
|
||||
readonly_fields = ["created_at", "total_price"]
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ from django.db import models
|
||||
class ProcurementStatus(models.TextChoices):
|
||||
"""Статусы заявки на закупку."""
|
||||
DRAFT = 'draft', 'Черновик'
|
||||
WAITING = 'waiting', 'Ожидает'
|
||||
PENDING = 'pending', 'На согласовании'
|
||||
APPROVED = 'approved', 'Одобрено'
|
||||
REJECTED = 'rejected', 'Отклонено'
|
||||
@ -25,6 +26,26 @@ class UrgencyLevel(models.TextChoices):
|
||||
CRITICAL = 'critical', 'Критическая'
|
||||
|
||||
|
||||
class ProcurementFulfillmentStatus(models.TextChoices):
|
||||
"""Рабочий статус исполнения заявки по позициям."""
|
||||
PENDING = 'pending', 'Ожидает обработки'
|
||||
PARTIALLY_ORDERED = 'partially_ordered', 'Заказано частично'
|
||||
ORDERED = 'ordered', 'Заказано'
|
||||
PARTIALLY_RECEIVED = 'partially_received', 'Прибыл частично'
|
||||
COMPLETED = 'completed', 'Выполнено'
|
||||
ISSUES = 'issues', 'Выполнено с проблемами'
|
||||
|
||||
|
||||
class ProcurementItemExecutionStatus(models.TextChoices):
|
||||
"""Рабочий статус отдельной позиции заявки."""
|
||||
PENDING = 'pending', 'Не выполнено'
|
||||
ORDERED = 'ordered', 'Заказано'
|
||||
REJECTED = 'rejected', 'Отказано'
|
||||
RECEIVED = 'received', 'Получено'
|
||||
COMPLETED_WITH_ISSUE = 'completed_with_issue', 'Выполнено с замечанием'
|
||||
EDITED = 'edited', 'Отредактировано'
|
||||
|
||||
|
||||
def get_default_approval_step_name(priority: int,
|
||||
resolver_type: str | None = None) -> str:
|
||||
if resolver_type == 'department_head':
|
||||
|
||||
@ -0,0 +1,124 @@
|
||||
# Generated by Codex on 2026-05-22
|
||||
|
||||
import django.core.validators
|
||||
from decimal import Decimal
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("employees", "0052_employee_attendance_aliases"),
|
||||
("procurement", "0009_approvalroute_min_amount"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="procurementrequest",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("draft", "Черновик"),
|
||||
("waiting", "Ожидает"),
|
||||
("pending", "На согласовании"),
|
||||
("approved", "Одобрено"),
|
||||
("rejected", "Отклонено"),
|
||||
("in_progress", "В работе"),
|
||||
("completed", "Завершено"),
|
||||
("cancelled", "Отменено"),
|
||||
],
|
||||
default="draft",
|
||||
max_length=20,
|
||||
verbose_name="Статус",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="procurementrequest",
|
||||
name="fulfillment_status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("pending", "Ожидает обработки"),
|
||||
("partially_ordered", "Заказано частично"),
|
||||
("ordered", "Заказано"),
|
||||
("partially_received", "Прибыл частично"),
|
||||
("completed", "Выполнено"),
|
||||
("issues", "Выполнено с проблемами"),
|
||||
],
|
||||
default="pending",
|
||||
max_length=30,
|
||||
verbose_name="Статус исполнения",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="procurementrequest",
|
||||
name="processing_department",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Отдел, который должен обработать заявку",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="processing_procurement_requests",
|
||||
to="employees.department",
|
||||
verbose_name="Отдел-исполнитель",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="procurementitem",
|
||||
name="actual_unit_price",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
max_digits=12,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(Decimal("0.01"))
|
||||
],
|
||||
verbose_name="Фактическая цена за единицу",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="procurementitem",
|
||||
name="execution_status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("pending", "Не выполнено"),
|
||||
("ordered", "Заказано"),
|
||||
("rejected", "Отказано"),
|
||||
("received", "Получено"),
|
||||
("completed_with_issue", "Выполнено с замечанием"),
|
||||
("edited", "Отредактировано"),
|
||||
],
|
||||
default="pending",
|
||||
max_length=30,
|
||||
verbose_name="Статус выполнения",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="procurementitem",
|
||||
name="expected_delivery_date",
|
||||
field=models.DateField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Ожидаемая дата поступления",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="procurementitem",
|
||||
name="executor_comment",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
verbose_name="Комментарий исполнителя",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="procurementitem",
|
||||
name="links",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="Список ссылок на позицию",
|
||||
verbose_name="Ссылки",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -27,6 +27,8 @@ from .constants import (
|
||||
ApprovalStatus,
|
||||
EquipmentStatus,
|
||||
MaintenanceType,
|
||||
ProcurementFulfillmentStatus,
|
||||
ProcurementItemExecutionStatus,
|
||||
ProcurementStatus,
|
||||
UrgencyLevel,
|
||||
get_default_approval_step_name,
|
||||
@ -55,6 +57,15 @@ class ProcurementRequest(models.Model):
|
||||
related_name="procurement_requests",
|
||||
verbose_name="Отдел",
|
||||
)
|
||||
processing_department = models.ForeignKey(
|
||||
Department,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="processing_procurement_requests",
|
||||
verbose_name="Отдел-исполнитель",
|
||||
help_text="Отдел, который должен обработать заявку",
|
||||
)
|
||||
requestor = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
@ -82,6 +93,12 @@ class ProcurementRequest(models.Model):
|
||||
choices=UrgencyLevel.choices,
|
||||
default=UrgencyLevel.MEDIUM,
|
||||
)
|
||||
fulfillment_status = models.CharField(
|
||||
"Статус исполнения",
|
||||
max_length=30,
|
||||
choices=ProcurementFulfillmentStatus.choices,
|
||||
default=ProcurementFulfillmentStatus.PENDING,
|
||||
)
|
||||
actual_cost = models.DecimalField(
|
||||
"Фактическая стоимость",
|
||||
max_digits=12,
|
||||
@ -144,7 +161,67 @@ class ProcurementRequest(models.Model):
|
||||
@property
|
||||
def is_editable(self):
|
||||
"""Можно ли редактировать заявку."""
|
||||
return self.status == ProcurementStatus.DRAFT
|
||||
return self.status in [
|
||||
ProcurementStatus.DRAFT,
|
||||
ProcurementStatus.WAITING,
|
||||
] and self.executor_id is None
|
||||
|
||||
def recalculate_fulfillment_status(self, save=True):
|
||||
"""Пересчитать рабочий статус заявки по статусам позиций."""
|
||||
item_statuses = list(
|
||||
self.items.values_list("execution_status", flat=True)
|
||||
)
|
||||
if not item_statuses:
|
||||
new_status = ProcurementFulfillmentStatus.PENDING
|
||||
elif any(
|
||||
item_status
|
||||
in {
|
||||
ProcurementItemExecutionStatus.REJECTED,
|
||||
ProcurementItemExecutionStatus.COMPLETED_WITH_ISSUE,
|
||||
ProcurementItemExecutionStatus.EDITED,
|
||||
}
|
||||
for item_status in item_statuses
|
||||
):
|
||||
new_status = ProcurementFulfillmentStatus.ISSUES
|
||||
elif all(
|
||||
item_status == ProcurementItemExecutionStatus.RECEIVED
|
||||
for item_status in item_statuses
|
||||
):
|
||||
new_status = ProcurementFulfillmentStatus.COMPLETED
|
||||
elif any(
|
||||
item_status == ProcurementItemExecutionStatus.RECEIVED
|
||||
for item_status in item_statuses
|
||||
):
|
||||
new_status = ProcurementFulfillmentStatus.PARTIALLY_RECEIVED
|
||||
elif all(
|
||||
item_status == ProcurementItemExecutionStatus.ORDERED
|
||||
for item_status in item_statuses
|
||||
):
|
||||
new_status = ProcurementFulfillmentStatus.ORDERED
|
||||
elif any(
|
||||
item_status == ProcurementItemExecutionStatus.ORDERED
|
||||
for item_status in item_statuses
|
||||
):
|
||||
new_status = ProcurementFulfillmentStatus.PARTIALLY_ORDERED
|
||||
else:
|
||||
new_status = ProcurementFulfillmentStatus.PENDING
|
||||
|
||||
if self.fulfillment_status == new_status:
|
||||
return new_status
|
||||
|
||||
self.fulfillment_status = new_status
|
||||
if save:
|
||||
update_fields = ["fulfillment_status", "updated_at"]
|
||||
if (
|
||||
new_status == ProcurementFulfillmentStatus.COMPLETED
|
||||
and self.status
|
||||
in [ProcurementStatus.WAITING, ProcurementStatus.IN_PROGRESS]
|
||||
):
|
||||
self.status = ProcurementStatus.COMPLETED
|
||||
self.completed_at = timezone.now()
|
||||
update_fields.extend(["status", "completed_at"])
|
||||
self.save(update_fields=update_fields)
|
||||
return new_status
|
||||
|
||||
|
||||
class ProcurementItem(models.Model):
|
||||
@ -184,6 +261,35 @@ class ProcurementItem(models.Model):
|
||||
blank=True,
|
||||
help_text="Ссылки на товар, контакты поставщика",
|
||||
)
|
||||
links = models.JSONField(
|
||||
"Ссылки",
|
||||
default=list,
|
||||
blank=True,
|
||||
help_text="Список ссылок на позицию",
|
||||
)
|
||||
expected_delivery_date = models.DateField(
|
||||
"Ожидаемая дата поступления",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
actual_unit_price = models.DecimalField(
|
||||
"Фактическая цена за единицу",
|
||||
max_digits=12,
|
||||
decimal_places=2,
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(Decimal("0.01"))],
|
||||
)
|
||||
execution_status = models.CharField(
|
||||
"Статус выполнения",
|
||||
max_length=30,
|
||||
choices=ProcurementItemExecutionStatus.choices,
|
||||
default=ProcurementItemExecutionStatus.PENDING,
|
||||
)
|
||||
executor_comment = models.TextField(
|
||||
"Комментарий исполнителя",
|
||||
blank=True,
|
||||
)
|
||||
equipment = models.OneToOneField(
|
||||
"Equipment",
|
||||
on_delete=models.SET_NULL,
|
||||
@ -208,6 +314,17 @@ class ProcurementItem(models.Model):
|
||||
"""Общая стоимость позиции."""
|
||||
return self.estimated_unit_price * self.quantity
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.request_id:
|
||||
self.request.recalculate_fulfillment_status(save=True)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
request = self.request
|
||||
result = super().delete(*args, **kwargs)
|
||||
request.recalculate_fulfillment_status(save=True)
|
||||
return result
|
||||
|
||||
|
||||
class ApprovalRoute(models.Model):
|
||||
"""Правило получения согласующего для этапа с заданным приоритетом."""
|
||||
|
||||
@ -13,6 +13,8 @@ from rest_framework.test import APIClient
|
||||
from employees.models import Department, Employee, EmployeeDepartment
|
||||
from procurement.constants import (
|
||||
ApprovalStatus,
|
||||
ProcurementFulfillmentStatus,
|
||||
ProcurementItemExecutionStatus,
|
||||
ProcurementStatus,
|
||||
UrgencyLevel,
|
||||
)
|
||||
@ -332,6 +334,46 @@ class TestProcurementRequestCreate:
|
||||
status.HTTP_403_FORBIDDEN
|
||||
]
|
||||
|
||||
def test_create_processing_department_request_skips_approvals(
|
||||
self, api_client, user, department, db
|
||||
):
|
||||
"""Заявка в отдел-исполнитель сразу попадает в очередь."""
|
||||
supply_department = Department.objects.create(
|
||||
name="Снабжение",
|
||||
description="Отдел снабжения",
|
||||
)
|
||||
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse('api:v1:procurement:procurementrequest-list')
|
||||
|
||||
response = api_client.post(
|
||||
url,
|
||||
{
|
||||
'title': 'Расходники для производства',
|
||||
'description': 'Нужны материалы для заказа',
|
||||
'department': department.id,
|
||||
'processing_department': supply_department.id,
|
||||
'urgency': UrgencyLevel.MEDIUM,
|
||||
'items': [
|
||||
{
|
||||
'name': 'Перчатки',
|
||||
'quantity': 10,
|
||||
'unit': 'упак',
|
||||
'estimated_unit_price': '500.00',
|
||||
'links': ['https://example.com/gloves'],
|
||||
},
|
||||
],
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data['status'] == ProcurementStatus.WAITING
|
||||
request_id = response.data['id']
|
||||
procurement_request = ProcurementRequest.objects.get(id=request_id)
|
||||
assert procurement_request.processing_department == supply_department
|
||||
assert procurement_request.approvals.count() == 0
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# ТЕСТЫ ДЕТАЛЬНОЙ ИНФОРМАЦИИ
|
||||
@ -499,6 +541,190 @@ class TestProcurementRequestUpdate:
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# ТЕСТЫ АДРЕСНЫХ ЗАЯВОК В ОТДЕЛ-ИСПОЛНИТЕЛЬ
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
class TestProcessingDepartmentWorkflow:
|
||||
"""Workflow для заявок, направленных в отдел-исполнитель."""
|
||||
|
||||
@pytest.fixture
|
||||
def supply_department(self, db):
|
||||
return Department.objects.create(
|
||||
name="Снабжение",
|
||||
description="Отдел снабжения",
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def supply_user(self, db, supply_department):
|
||||
employee = Employee.objects.create_user(
|
||||
email="supply@example.com",
|
||||
password="testpass123",
|
||||
phone_number="+79998888888",
|
||||
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 outsider(self, db):
|
||||
return Employee.objects.create_user(
|
||||
email="outsider@example.com",
|
||||
password="testpass123",
|
||||
phone_number="+79998888889",
|
||||
first_name="Чужой",
|
||||
last_name="Сотрудник",
|
||||
is_active=True,
|
||||
email_verified=True,
|
||||
send_activation_email=False,
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def processing_request(
|
||||
self, department, user, supply_department
|
||||
):
|
||||
return ProcurementRequest.objects.create(
|
||||
title="Заявка на расходники",
|
||||
description="Нужны расходные материалы",
|
||||
department=department,
|
||||
processing_department=supply_department,
|
||||
requestor=user,
|
||||
status=ProcurementStatus.WAITING,
|
||||
urgency=UrgencyLevel.MEDIUM,
|
||||
)
|
||||
|
||||
def test_available_shows_processing_department_requests(
|
||||
self, api_client, supply_user, processing_request, procurement_item_factory
|
||||
):
|
||||
procurement_item_factory(request=processing_request)
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
url = reverse('api:v1:procurement:procurementrequest-available')
|
||||
|
||||
response = api_client.get(url)
|
||||
|
||||
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_outsider_cannot_start_processing_department_request(
|
||||
self, api_client, outsider, processing_request, procurement_item_factory
|
||||
):
|
||||
procurement_item_factory(request=processing_request)
|
||||
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 is None
|
||||
assert processing_request.status == ProcurementStatus.WAITING
|
||||
|
||||
def test_processing_department_user_can_start_work_and_blocks_author_edit(
|
||||
self, api_client, user, supply_user, processing_request,
|
||||
procurement_item_factory
|
||||
):
|
||||
procurement_item_factory(request=processing_request)
|
||||
start_url = reverse(
|
||||
'api:v1:procurement:procurementrequest-start-work',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
detail_url = reverse(
|
||||
'api:v1:procurement:procurementrequest-detail',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
start_response = api_client.post(start_url)
|
||||
assert start_response.status_code == status.HTTP_200_OK
|
||||
|
||||
processing_request.refresh_from_db()
|
||||
assert processing_request.executor == supply_user
|
||||
assert processing_request.status == ProcurementStatus.IN_PROGRESS
|
||||
|
||||
api_client.force_authenticate(user=user)
|
||||
edit_response = api_client.patch(
|
||||
detail_url,
|
||||
{'title': 'Нельзя менять'},
|
||||
format='json',
|
||||
)
|
||||
assert edit_response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_item_status_recalculates_request_fulfillment_status(
|
||||
self, api_client, supply_user, processing_request,
|
||||
procurement_item_factory
|
||||
):
|
||||
item = procurement_item_factory(request=processing_request)
|
||||
processing_request.executor = supply_user
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.save()
|
||||
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
item_url = reverse(
|
||||
'api:v1:procurement:procurementitem-detail',
|
||||
kwargs={'pk': item.id},
|
||||
)
|
||||
|
||||
response = api_client.patch(
|
||||
item_url,
|
||||
{'execution_status': ProcurementItemExecutionStatus.RECEIVED},
|
||||
format='json',
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
processing_request.refresh_from_db()
|
||||
assert (
|
||||
processing_request.fulfillment_status
|
||||
== ProcurementFulfillmentStatus.COMPLETED
|
||||
)
|
||||
assert processing_request.status == ProcurementStatus.COMPLETED
|
||||
|
||||
def test_mark_all_received_completes_processing_request(
|
||||
self, api_client, supply_user, processing_request,
|
||||
procurement_item_factory
|
||||
):
|
||||
item_1 = procurement_item_factory(request=processing_request)
|
||||
item_2 = procurement_item_factory(
|
||||
request=processing_request,
|
||||
name="Лампы",
|
||||
)
|
||||
processing_request.executor = supply_user
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.save()
|
||||
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:procurementrequest-mark-all-received',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
processing_request.refresh_from_db()
|
||||
item_1.refresh_from_db()
|
||||
item_2.refresh_from_db()
|
||||
assert item_1.execution_status == ProcurementItemExecutionStatus.RECEIVED
|
||||
assert item_2.execution_status == ProcurementItemExecutionStatus.RECEIVED
|
||||
assert (
|
||||
processing_request.fulfillment_status
|
||||
== ProcurementFulfillmentStatus.COMPLETED
|
||||
)
|
||||
assert processing_request.status == ProcurementStatus.COMPLETED
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# ТЕСТЫ WORKFLOW ДЕЙСТВИЙ
|
||||
# ==============================================================================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user