"""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)