409 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""ViewSets для API модуля закупок."""
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils import timezone
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.constants import (
ProcurementStatus,
)
from procurement.models import (
ProcurementItem,
ProcurementActivityAction,
ProcurementActivityObjectKind,
ProcurementRequest,
ProcurementRequestView,
)
from procurement.activity import create_procurement_activity
from procurement.notifications.config import NotificationVerbs
from procurement.services import ProcurementApprovalResolver
from ..permissions import (
CanApproveProcurementRequest,
CanManageProcurementRequest,
)
from ..serializers import (
ProcurementRequestCreateSerializer,
ProcurementRequestDetailSerializer,
ProcurementRequestActivitySerializer,
ProcurementRequestListSerializer,
)
from .mixins import RequestCommentsMixin, RequestExecutionMixin, RequestWorkflowMixin
class ProcurementRequestViewSet(
RequestWorkflowMixin,
RequestCommentsMixin,
RequestExecutionMixin,
viewsets.ModelViewSet,
):
"""ViewSet для заявок на закупку."""
queryset = ProcurementRequest.objects.select_related(
"department",
"processing_department",
"requestor",
"executor",
).prefetch_related(
"items__attachments__uploaded_by",
"approvals__approver",
)
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filterset_fields = {
"status": ["exact", "in"],
"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
elif self.action in [
"retrieve",
"submit",
"approve",
"reject",
"start_work",
"complete",
"cancel",
"mark_all_received",
"notify_arrival",
"set_viewed",
"approval_options",
"activity",
]:
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)
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
}
procurement_request = serializer.save()
changed_fields = [
field
for field in tracked_fields
if before[field] != getattr(procurement_request, field)
]
if changed_fields:
create_procurement_activity(
procurement_request,
self.request.user,
ProcurementActivityAction.UPDATED,
object_kind=ProcurementActivityObjectKind.REQUEST,
object_id=procurement_request.pk,
metadata={"fields": changed_fields},
)
def get_permissions(self):
"""Выбрать права доступа в зависимости от действия."""
if self.action in ["approve", "reject"]:
permission_classes = [CanApproveProcurementRequest]
elif self.action in [
"submit",
"set_viewed",
"create_options",
"approval_options",
]:
permission_classes = [permissions.IsAuthenticated]
else:
# Для create, update, delete и остальных действий
permission_classes = [CanManageProcurementRequest]
return [permission() for permission in permission_classes]
def get_queryset(self):
"""Фильтровать заявки в зависимости от роли пользователя и scope."""
from datetime import timedelta
from django.contrib.contenttypes.models import ContentType
from django.db.models import (
BooleanField,
CharField,
Count,
DateTimeField,
Exists,
IntegerField,
OuterRef,
Subquery,
Value,
)
from django.db.models.functions import Cast, Coalesce
from django.utils.dateparse import parse_date
from communications.models import Chat
from notifications.models import Notification
ct = ContentType.objects.get_for_model(ProcurementRequest)
comments_sub = (
Chat.objects.filter(
type="comments",
context_content_type=ct,
context_object_id=OuterRef("pk"),
)
.annotate(msg_count=Count("messages", filter=Q(messages__is_deleted=False)))
.values("msg_count")[:1]
)
arrival_notice_sub = (
Notification.objects.filter(
verb=NotificationVerbs.ARRIVAL_NOTICE,
target_content_type=ct,
target_object_id=Cast(
OuterRef("pk"),
output_field=CharField(),
),
)
.order_by("-timestamp")
.values("timestamp")[:1]
)
last_defective_at_sub = (
ProcurementItem.objects.filter(
request_id=OuterRef("pk"),
defective_at__isnull=False,
)
.order_by("-defective_at")
.values("defective_at")[:1]
)
user = self.request.user
viewed_annotation = (
Exists(
ProcurementRequestView.objects.filter(
request_id=OuterRef("pk"),
user=user,
is_viewed=True,
)
)
if user and user.is_authenticated
else Value(False, output_field=BooleanField())
)
queryset = (
super()
.get_queryset()
.annotate(
comments_count=Coalesce(
Subquery(comments_sub, output_field=IntegerField()),
Value(0),
output_field=IntegerField(),
),
is_viewed_for_current_user=viewed_annotation,
last_arrival_notice_at=Subquery(
arrival_notice_sub,
output_field=DateTimeField(),
),
last_defective_at=Subquery(
last_defective_at_sub,
output_field=DateTimeField(),
),
)
.annotate(
default_sort_at=Coalesce(
"last_defective_at",
"created_at",
output_field=DateTimeField(),
),
)
)
scope = self.request.query_params.get("scope", None)
period = self.request.query_params.get("period", None)
date_from = self.request.query_params.get("date_from", None)
date_to = self.request.query_params.get("date_to", None)
participant_department_ids = (
ProcurementApprovalResolver.get_user_department_participant_ids(
user,
)
)
# Обработка scope параметра
if scope == "mine":
# Только мои заявки (где я автор)
queryset = queryset.filter(requestor=user)
elif scope == "department":
# Заявки отделов, где пользователь участник или role-only участник.
queryset = queryset.filter(department_id__in=participant_department_ids)
elif scope == "processing_department":
# Заявки, направленные в отделы, где пользователь участвует.
queryset = queryset.filter(
processing_department_id__in=participant_department_ids,
)
elif scope == "my_work":
# Заявки, которые я взял в работу (где я исполнитель)
queryset = queryset.filter(executor=user)
elif scope == "available":
queryset = queryset.filter(
status__in=ProcurementApprovalResolver.WORK_ASSIGNABLE_STATUSES,
).exclude(
executor=user,
)
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(
processing_department_id__in=participant_department_ids,
)
| Q(
processing_department__isnull=True,
)
)
elif scope == "all":
# Все заявки - применяем стандартную логику прав
pass
else:
# Если scope не указан, применяем стандартную логику
# Админы и пользователи с модельными правами видят все
if user.is_superuser or user.is_staff:
pass
# Пользователи с любыми модельными правами на заявки видят все
elif (
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:
# Показываем: свои заявки + заявки отдела + где я approver
queryset = queryset.filter(
Q(requestor=user)
| Q(department_id__in=participant_department_ids)
| Q(processing_department_id__in=participant_department_ids)
| Q(approvals__approver=user)
| Q(
status=ProcurementStatus.APPROVED,
processing_department__isnull=True,
)
)
# Фильтрация по периоду
if period:
now = timezone.now()
if period == "today":
start_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
queryset = queryset.filter(created_at__gte=start_date)
elif period == "week":
start_date = now - timedelta(days=7)
queryset = queryset.filter(created_at__gte=start_date)
elif period == "month":
start_date = now - timedelta(days=30)
queryset = queryset.filter(created_at__gte=start_date)
elif period == "quarter":
start_date = now - timedelta(days=90)
queryset = queryset.filter(created_at__gte=start_date)
if date_from:
parsed_date_from = parse_date(date_from)
if parsed_date_from:
queryset = queryset.filter(created_at__date__gte=parsed_date_from)
if date_to:
parsed_date_to = parse_date(date_to)
if parsed_date_to:
queryset = queryset.filter(created_at__date__lte=parsed_date_to)
if not self.request.query_params.get("ordering"):
queryset = queryset.order_by("-default_sort_at", "-id")
return queryset.distinct()
def _attach_linked_task_payloads(
self,
requests: list[ProcurementRequest],
) -> None:
"""Предзагрузить компактные бейджи задач для списка закупок."""
if not requests:
return
user = getattr(self.request, "user", None)
request_ids = [request_obj.id for request_obj in requests]
mapping = {request_id: [] for request_id in request_ids}
if user and user.is_authenticated:
from tasks.access import task_board_access_q
from tasks.models import (
TaskBoard,
TaskLinkedObject,
TaskLinkedObjectKind,
)
request_ct = ContentType.objects.get_for_model(ProcurementRequest)
accessible_boards = TaskBoard.objects.filter(
is_archived=False,
).filter(task_board_access_q(user))
links = (
TaskLinkedObject.objects.filter(
kind=TaskLinkedObjectKind.PROCUREMENT_REQUEST,
content_type=request_ct,
object_id__in=request_ids,
task__board__in=accessible_boards,
)
.select_related("task", "task__board", "task__column")
.order_by("object_id", "task__title", "task_id")
)
for link in links:
mapping.setdefault(link.object_id, []).append(
{
"link_id": link.id,
"id": link.task_id,
"title": link.task.title,
"board_id": link.task.board_id,
"board_name": link.task.board.name,
"column_id": link.task.column_id,
"column_name": link.task.column.name,
"column_color": link.task.column.color,
"priority": link.task.priority,
"priority_display": link.task.get_priority_display(),
}
)
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)