248 lines
8.8 KiB
Python

"""ViewSet composition for procurement requests."""
from django.db import transaction
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, permissions, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from procurement.activity import create_procurement_activity
from procurement.models import (
ProcurementActivityAction,
ProcurementActivityObjectKind,
ProcurementRequest,
)
from projects.models import Project
from ..permissions import (
CanApproveProcurementRequest,
CanAssignProcurementProjects,
CanManageProcurementRequest,
)
from ..serializers import (
ProcurementProjectsSerializer,
ProcurementRequestActivitySerializer,
ProcurementRequestCreateSerializer,
ProcurementRequestDetailSerializer,
ProcurementRequestListSerializer,
)
from .mixins import (
ProcurementRequestQueryMixin,
RequestCommentsMixin,
RequestExecutionMixin,
RequestWorkflowMixin,
)
class ProcurementRequestViewSet(
ProcurementRequestQueryMixin,
RequestWorkflowMixin,
RequestCommentsMixin,
RequestExecutionMixin,
viewsets.ModelViewSet,
):
"""ViewSet для заявок на закупку."""
queryset = ProcurementRequest.objects.select_related(
"department",
"processing_department",
"requestor",
"executor",
).prefetch_related(
"projects",
"items__attachments__uploaded_by",
"approvals__approver",
)
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filterset_fields = {
"status": ["exact", "in"],
"request_type": ["exact"],
"urgency": ["exact"],
"department": ["exact"],
"processing_department": ["exact"],
"requestor": ["exact"],
"executor": ["exact"],
"fulfillment_status": ["exact"],
}
search_fields = ["title", "description"]
ordering_fields = ["created_at", "status"]
ordering = None
def get_serializer_class(self):
"""Выбрать сериализатор в зависимости от действия."""
if self.action == "create":
return ProcurementRequestCreateSerializer
if self.action in {
"retrieve",
"submit",
"approve",
"reject",
"start_work",
"complete",
"cancel",
"mark_all_received",
"notify_arrival",
"set_viewed",
"set_deferred",
"approval_options",
"activity",
"projects",
}:
return ProcurementRequestDetailSerializer
return ProcurementRequestListSerializer
@action(detail=True, methods=["get"], url_path="activity")
def activity(self, request, pk=None):
procurement_request = self.get_object()
queryset = procurement_request.activities.select_related("actor").order_by(
"-created_at",
"-id",
)
serializer = ProcurementRequestActivitySerializer(queryset, many=True)
return Response(serializer.data)
@action(detail=True, methods=["get"], url_path="project-options")
def project_options(self, request, pk=None):
"""Compact company project directory for authorized purchasing staff."""
self.get_object()
return Response(
list(Project.objects.order_by("name", "pk").values("id", "name"))
)
@action(detail=True, methods=["patch"], url_path="projects")
def projects(self, request, pk=None):
"""Change only project associations; usable after approval or completion."""
payload = ProcurementProjectsSerializer(
data=request.data, context={"request": request}
)
payload.is_valid(raise_exception=True)
with transaction.atomic():
visible_request = self.get_object()
procurement_request = ProcurementRequest.objects.select_for_update().get(
pk=visible_request.pk
)
old_projects = list(
procurement_request.projects.order_by("id").values("id", "name")
)
selected = payload.validated_data["projects"]
next_ids = {project.pk for project in selected}
if {project["id"] for project in old_projects} != next_ids:
procurement_request.projects.set(selected)
procurement_request.save(update_fields=["updated_at"])
new_projects = list(
procurement_request.projects.order_by("id").values("id", "name")
)
create_procurement_activity(
procurement_request,
request.user,
ProcurementActivityAction.UPDATED,
object_kind=ProcurementActivityObjectKind.REQUEST,
object_id=procurement_request.pk,
metadata={
"fields": ["projects"],
"previous_projects": old_projects,
"new_projects": new_projects,
},
)
result = self.get_serializer(procurement_request)
return Response(result.data)
def perform_update(self, serializer):
tracked_fields = [
"title",
"description",
"department_id",
"processing_department_id",
"urgency",
"actual_cost",
]
before = {
field: getattr(serializer.instance, field) for field in tracked_fields
}
old_projects = list(
serializer.instance.projects.order_by("id").values("id", "name")
)
procurement_request = serializer.save()
new_projects = list(
procurement_request.projects.order_by("id").values("id", "name")
)
changed_fields = [
field
for field in tracked_fields
if before[field] != getattr(procurement_request, field)
]
if old_projects != new_projects:
changed_fields.append("projects")
if changed_fields:
metadata = {"fields": changed_fields}
if old_projects != new_projects:
metadata.update(
previous_projects=old_projects,
new_projects=new_projects,
)
create_procurement_activity(
procurement_request,
self.request.user,
ProcurementActivityAction.UPDATED,
object_kind=ProcurementActivityObjectKind.REQUEST,
object_id=procurement_request.pk,
metadata=metadata,
)
def get_permissions(self):
"""Выбрать права доступа в зависимости от действия."""
if self.action in {"approve", "reject"}:
permission_classes = [CanApproveProcurementRequest]
elif self.action in {"projects", "project_options"}:
permission_classes = [CanAssignProcurementProjects]
elif self.action in {
"submit",
"set_viewed",
"set_deferred",
"create_options",
"approval_options",
}:
permission_classes = [permissions.IsAuthenticated]
else:
permission_classes = [CanManageProcurementRequest]
return [permission() for permission in permission_classes]
def _attach_linked_task_payloads(
self,
requests: list[ProcurementRequest],
) -> None:
"""Предзагрузить компактные бейджи задач для списка закупок."""
if not requests:
return
user = getattr(self.request, "user", None)
from tasks.models import TaskLinkedObjectKind
from tasks.presentation import linked_task_badges_by_object
mapping = linked_task_badges_by_object(
model=ProcurementRequest,
kind=TaskLinkedObjectKind.PROCUREMENT_REQUEST,
object_ids=[request_obj.id for request_obj in requests],
user=user,
)
for request_obj in requests:
request_obj._linked_task_payloads = mapping.get(request_obj.id, [])
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
self._attach_linked_task_payloads(list(page))
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
requests = list(queryset)
self._attach_linked_task_payloads(requests)
serializer = self.get_serializer(requests, many=True)
return Response(serializer.data)