diff --git a/api_v1/views/printing/views.py b/api_v1/views/printing/views.py index 2a6ea35..a714f60 100644 --- a/api_v1/views/printing/views.py +++ b/api_v1/views/printing/views.py @@ -1,6 +1,7 @@ """ Printing API ViewSet """ + from rest_framework import viewsets, filters, status from rest_framework.decorators import action from rest_framework.exceptions import ValidationError as DRFValidationError @@ -39,12 +40,12 @@ from .mixins import CustomerVisibilityFilterMixin class IsPrintingFactory(BasePermission): """自定义权限类,允许印染工厂用户访问""" - message = '您没有访问印染订单的权限' - + message = "您没有访问印染订单的权限" + def has_permission(self, request, view): if not request.user.is_authenticated: return False - if hasattr(request.user, 'employee'): + if hasattr(request.user, "employee"): return request.user.employee.merchant.type == MerchantTypeEnum.FACTORY return False @@ -52,43 +53,95 @@ class IsPrintingFactory(BasePermission): class HasInvalidatePrintingOrderPermission(BasePermission): """自定义权限类,检查用户是否有作废印染订单的权限""" - message = '您没有权限作废订单' - + message = "您没有权限作废订单" + def has_permission(self, request, view): if not request.user.is_authenticated: return False - return request.user.has_perm('printing.can_invalidate_printingorder') + return request.user.has_perm("printing.can_invalidate_printingorder") class HasActivatePrintingOrderPermission(BasePermission): """自定义权限类,检查用户是否有恢复印染订单的权限""" - message = '您没有权限恢复订单' - + message = "您没有权限恢复订单" + def has_permission(self, request, view): if not request.user.is_authenticated: return False - return request.user.has_perm('printing.can_activate_printingorder') + return request.user.has_perm("printing.can_activate_printingorder") class PrintingOrderFilterSet(django_filters.FilterSet): """印染订单过滤器""" - external_order_id = django_filters.CharFilter(lookup_expr='icontains') - customer_name = django_filters.CharFilter(field_name='customer__name', lookup_expr='icontains') - customer_phone = django_filters.CharFilter(field_name='customer__phone', lookup_expr='icontains') - fabric = django_filters.CharFilter(lookup_expr='icontains') + + external_order_id = django_filters.CharFilter(lookup_expr="icontains") + customer_name = django_filters.CharFilter( + field_name="customer__name", lookup_expr="icontains" + ) + customer_phone = django_filters.CharFilter( + field_name="customer__phone", lookup_expr="icontains" + ) + fabric = django_filters.CharFilter(lookup_expr="icontains") is_urgent = django_filters.BooleanFilter() is_fabric_received = django_filters.BooleanFilter() is_invalid = django_filters.BooleanFilter() - area = django_filters.CharFilter(lookup_expr='icontains') - outgoing_date_from = django_filters.CharFilter(method='filter_outgoing_date_from') - outgoing_date_to = django_filters.CharFilter(method='filter_outgoing_date_to') - created_date_from = django_filters.DateFilter(field_name='created_at', lookup_expr='gte') - created_date_to = django_filters.DateFilter(field_name='created_at', method='filter_created_date_to') - + area = django_filters.CharFilter(lookup_expr="icontains") + outgoing_date_from = django_filters.CharFilter(method="filter_outgoing_date_from") + outgoing_date_to = django_filters.CharFilter(method="filter_outgoing_date_to") + created_date_from = django_filters.DateFilter( + field_name="created_at", lookup_expr="gte" + ) + created_date_to = django_filters.DateFilter( + field_name="created_at", method="filter_created_date_to" + ) + # 支持同时搜索 human_id 和 external_order_id + order_number = django_filters.CharFilter(method="filter_order_number") + class Meta: model = models.PrintingOrder - fields = ['customer', 'is_urgent', 'is_fabric_received', 'is_invalid', 'external_order_id'] + fields = [ + "customer", + "is_urgent", + "is_fabric_received", + "is_invalid", + "external_order_id", + "order_number", + ] + + def filter_order_number(self, queryset, name, value): + """ + 按订单号过滤(支持 human_id 和 external_order_id) + + 同时搜索: + 1. external_order_id(模糊匹配) + 2. id(从 human_id 格式年月日+ID 中提取) + + 例如:order_number=20260323001505 + - 匹配 external_order_id 包含 "20260323001505" + - 匹配 id=1505(提取最后6位) + """ + if not value: + return queryset + + from django.db.models import Q + + # 构建查询条件 + q_objects = Q(external_order_id__icontains=value) + + # 尝试从 human_id 格式提取 ID + # human_id 格式:年月日 + ID(如:20260323001505) + if len(value) >= 6: + # 尝试提取最后6位作为 ID + id_part = value[-6:].lstrip("0") # 去除前导零 + if id_part.isdigit(): + q_objects |= Q(id=int(id_part)) + + # 如果 value 是纯数字,也尝试直接匹配 ID + if value.isdigit(): + q_objects |= Q(id=int(value)) + + return queryset.filter(q_objects) @staticmethod def _parse_date_or_datetime(value: str | None) -> tuple[datetime | None, bool]: @@ -100,7 +153,7 @@ class PrintingOrderFilterSet(django_filters.FilterSet): - has_time=True:输入包含时间(datetime) - has_time=False:输入为纯日期(date-only) """ - s = (value or '').strip() + s = (value or "").strip() if not s: return None, False @@ -127,13 +180,13 @@ class PrintingOrderFilterSet(django_filters.FilterSet): dt, _has_time = self._parse_date_or_datetime(value) if dt is None: # 与 DRF/DjangoFilterBackend 行为保持一致:参数非法返回 400 - raise ValidationError('outgoing_date_from 必须是日期或日期时间') + raise ValidationError("outgoing_date_from 必须是日期或日期时间") return queryset.filter(outgoing_date__gte=dt) def filter_outgoing_date_to(self, queryset, name, value): dt, has_time = self._parse_date_or_datetime(value) if dt is None: - raise ValidationError('outgoing_date_to 必须是日期或日期时间') + raise ValidationError("outgoing_date_to 必须是日期或日期时间") # 纯日期:包含整天 => outgoing_date < 次日 00:00:00 if not has_time: @@ -149,24 +202,25 @@ class PrintingOrderFilterSet(django_filters.FilterSet): """ if value: from datetime import timedelta + next_day = value + timedelta(days=1) return queryset.filter(created_at__lt=next_day) return queryset -@method_decorator(cache_page(20), name='list') +@method_decorator(cache_page(20), name="list") class PrintingOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): """ 印染订单 ViewSet - + 提供印染订单的增改查功能(不支持删除) - + list: 获取印染订单列表 retrieve: 获取印染订单详情 create: 创建印染订单 update: 更新印染订单 partial_update: 部分更新印染订单 - + 查询参数: - customer: 客户ID - external_order_id: 外部订单编号(模糊查询) @@ -183,188 +237,193 @@ class PrintingOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): - created_date_to: 创建日期结束 - search: 全文搜索(客户名称、面料、地区、工艺) - ordering: 排序字段 - + 返回字段说明: - jobs_status_summary: 订单下所有印染任务按下一待执行状态分组的数量汇总 格式: [{"state_name": "进度一", "state_id": 1, "count": 3}, ...] - jobs_last_status_summary: 订单下所有印染任务按最后完成状态分组的数量汇总 格式: [{"state_name": "进度一", "count": 3}, ...] - + 权限控制: - 默认按客户可见性过滤(员工只能看到自己负责的客户的订单) - 拥有 printing.view_all_printingorders 权限可突破此限制 """ + queryset = models.PrintingOrder.objects.all() - customer_field = 'customer' - view_all_permission = 'printing.view_all_printingorders' + customer_field = "customer" + view_all_permission = "printing.view_all_printingorders" permission_classes = [DjangoModelPermissions] - filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] - filterset_class = PrintingOrderFilterSet - search_fields = ['customer__name', 'fabric', 'area', 'craft', 'description'] - ordering_fields = [ - 'id', 'created_at', 'updated_at', 'outgoing_date', - 'is_urgent', 'is_fabric_received', 'is_invalid' + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + filters.OrderingFilter, ] - ordering = ['-created_at'] + filterset_class = PrintingOrderFilterSet + search_fields = ["customer__name", "fabric", "area", "craft", "description"] + ordering_fields = [ + "id", + "created_at", + "updated_at", + "outgoing_date", + "is_urgent", + "is_fabric_received", + "is_invalid", + ] + ordering = ["-created_at"] def get_permissions(self): permissions = super().get_permissions() + [IsPrintingFactory()] - if self.action == 'invalidate': + if self.action == "invalidate": permissions.append(HasInvalidatePrintingOrderPermission()) - elif self.action == 'activate': + elif self.action == "activate": permissions.append(HasActivatePrintingOrderPermission()) return permissions - + def get_serializer_class(self): """根据动作选择序列化器""" - if self.action == 'list': + if self.action == "list": return PrintingOrderListSerializer - elif self.action in ['create', 'update', 'partial_update']: + elif self.action in ["create", "update", "partial_update"]: return PrintingOrderCreateUpdateSerializer else: # retrieve return PrintingOrderDetailSerializer - + def get_queryset(self): """优化查询""" queryset = super().get_queryset() # 默认不返回作废订单(仅 list);如显式传入 is_invalid 参数,则交由 FilterSet 处理 # 注意:不能影响 activate/invalidate 等 action,否则会因 get_object() 被过滤而 404。 - if self.action == 'list' and self.request and 'is_invalid' not in self.request.query_params: + if ( + self.action == "list" + and self.request + and "is_invalid" not in self.request.query_params + ): queryset = queryset.filter(is_invalid=False) - if self.action in ['list', 'retrieve']: - queryset = queryset.select_related('customer') + if self.action in ["list", "retrieve"]: + queryset = queryset.select_related("customer") # 预取 printing_jobs 及其 business_object,用于状态汇总统计 # 同时预取 state_logs 和 process 相关数据以减少 job.status 属性调用时的 N+1 查询 queryset = queryset.prefetch_related( Prefetch( - 'printing_jobs', + "printing_jobs", queryset=models.PrintingJob.objects.select_related( - 'business_object', - 'business_object__process', + "business_object", + "business_object__process", ).prefetch_related( - 'business_object__state_logs', - 'business_object__process__process_nodes', - 'business_object__process__process_nodes__state', - ) + "business_object__state_logs", + "business_object__process__process_nodes", + "business_object__process__process_nodes__state", + ), ) ) return queryset - + def destroy(self, request, *args, **kwargs): """禁用删除操作""" return Response( - {'detail': '印染订单不支持删除操作,请使用作废功能'}, - status=status.HTTP_405_METHOD_NOT_ALLOWED + {"detail": "印染订单不支持删除操作,请使用作废功能"}, + status=status.HTTP_405_METHOD_NOT_ALLOWED, ) - - @action(detail=True, methods=['post']) + + @action(detail=True, methods=["post"]) def invalidate(self, request, pk=None): """ 作废订单 - + 需要权限: printing.can_invalidate_printingorder """ # 检查权限 - if not request.user.has_perm('printing.can_invalidate_printingorder'): + if not request.user.has_perm("printing.can_invalidate_printingorder"): return Response( - {'detail': '您没有权限作废订单'}, - status=status.HTTP_403_FORBIDDEN + {"detail": "您没有权限作废订单"}, status=status.HTTP_403_FORBIDDEN ) - + printing_order = self.get_object() - + if printing_order.is_invalid: return Response( - {'detail': '该订单已经作废'}, - status=status.HTTP_400_BAD_REQUEST + {"detail": "该订单已经作废"}, status=status.HTTP_400_BAD_REQUEST ) - + printing_order.is_invalid = True printing_order.save() - + serializer = PrintingOrderDetailSerializer(printing_order) - return Response({ - 'detail': '订单已作废', - 'data': serializer.data - }) - - @action(detail=True, methods=['post']) + return Response({"detail": "订单已作废", "data": serializer.data}) + + @action(detail=True, methods=["post"]) def activate(self, request, pk=None): """ 恢复订单 - + 需要权限: printing.can_activate_printingorder """ # 检查权限 - if not request.user.has_perm('printing.can_activate_printingorder'): + if not request.user.has_perm("printing.can_activate_printingorder"): return Response( - {'detail': '您没有权限恢复订单'}, - status=status.HTTP_403_FORBIDDEN + {"detail": "您没有权限恢复订单"}, status=status.HTTP_403_FORBIDDEN ) - + printing_order = self.get_object() - + if not printing_order.is_invalid: return Response( - {'detail': '该订单未作废,无需恢复'}, - status=status.HTTP_400_BAD_REQUEST + {"detail": "该订单未作废,无需恢复"}, status=status.HTTP_400_BAD_REQUEST ) - + printing_order.is_invalid = False printing_order.save() - + serializer = PrintingOrderDetailSerializer(printing_order) - return Response({ - 'detail': '订单已恢复', - 'data': serializer.data - }) - - @action(detail=True, methods=['post']) + return Response({"detail": "订单已恢复", "data": serializer.data}) + + @action(detail=True, methods=["post"]) def mark_fabric_received(self, request, pk=None): """标记布料已收""" printing_order = self.get_object() - + if printing_order.is_fabric_received: return Response( - {'detail': '布料已经标记为已收'}, - status=status.HTTP_400_BAD_REQUEST + {"detail": "布料已经标记为已收"}, status=status.HTTP_400_BAD_REQUEST ) - + printing_order.is_fabric_received = True printing_order.save() - + serializer = PrintingOrderDetailSerializer(printing_order) - return Response({ - 'detail': '已标记布料已收', - 'data': serializer.data - }) + return Response({"detail": "已标记布料已收", "data": serializer.data}) class PrintingJobFilterSet(django_filters.FilterSet): """印染款式明细过滤器""" + printing_order = django_filters.NumberFilter() - external_order_id = django_filters.CharFilter(field_name='printing_order__external_order_id', lookup_expr='icontains') + external_order_id = django_filters.CharFilter( + field_name="printing_order__external_order_id", lookup_expr="icontains" + ) product = django_filters.NumberFilter() - product_name = django_filters.CharFilter(field_name='product__name', lookup_expr='icontains') - unit = django_filters.CharFilter(lookup_expr='icontains') - quantity_min = django_filters.NumberFilter(field_name='quantity', lookup_expr='gte') - quantity_max = django_filters.NumberFilter(field_name='quantity', lookup_expr='lte') - pieces_min = django_filters.NumberFilter(field_name='pieces', lookup_expr='gte') - pieces_max = django_filters.NumberFilter(field_name='pieces', lookup_expr='lte') - + product_name = django_filters.CharFilter( + field_name="product__name", lookup_expr="icontains" + ) + unit = django_filters.CharFilter(lookup_expr="icontains") + quantity_min = django_filters.NumberFilter(field_name="quantity", lookup_expr="gte") + quantity_max = django_filters.NumberFilter(field_name="quantity", lookup_expr="lte") + pieces_min = django_filters.NumberFilter(field_name="pieces", lookup_expr="gte") + pieces_max = django_filters.NumberFilter(field_name="pieces", lookup_expr="lte") + class Meta: model = models.PrintingJob - fields = ['printing_order', 'product', 'external_order_id'] + fields = ["printing_order", "product", "external_order_id"] class PrintingJobViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): """ 印染款式明细 ViewSet - + 提供印染款式明细的增改查功能(不支持删除) - + list: 获取款式明细列表 retrieve: 获取款式明细详情 create: 创建款式明细 @@ -374,12 +433,12 @@ class PrintingJobViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): step_back_one_state: 回退一步 completed_states: 查询已完成的流程列表 timeline: 获取流程时间线 - + 注意:状态流转接口是对 stateflow 模块的便捷包装 - 前端只需提供 printing_job 的 ID - 系统会自动查找关联的 business_object 并进行状态流转 - 也可以直接使用 stateflow 的原始接口(需要 business_object_id) - + 查询参数: - printing_order: 印染订单ID - external_order_id: 外部订单编号(按所属订单模糊查询) @@ -392,41 +451,47 @@ class PrintingJobViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): - pieces_max: 最大件数 - search: 全文搜索(产品名称、单位、尺寸、备注) - ordering: 排序字段 - + 权限控制: - 默认按客户可见性过滤(通过 printing_order.customer) - 拥有 printing.view_all_printingorders 权限可突破此限制 """ + queryset = models.PrintingJob.objects.all() - customer_field = 'printing_order__customer' - view_all_permission = 'printing.view_all_printingorders' + customer_field = "printing_order__customer" + view_all_permission = "printing.view_all_printingorders" permission_classes = [DjangoModelPermissions, IsPrintingFactory] - filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + filters.OrderingFilter, + ] filterset_class = PrintingJobFilterSet - search_fields = ['product__name', 'unit', 'size', 'description'] - ordering_fields = ['id', 'created_at', 'updated_at', 'quantity', 'pieces'] - ordering = ['-created_at'] - + search_fields = ["product__name", "unit", "size", "description"] + ordering_fields = ["id", "created_at", "updated_at", "quantity", "pieces"] + ordering = ["-created_at"] + def get_serializer_class(self): """根据动作选择序列化器""" - if self.action == 'list': + if self.action == "list": return PrintingJobListSerializer - elif self.action in ['create', 'update', 'partial_update']: + elif self.action in ["create", "update", "partial_update"]: return PrintingJobCreateUpdateSerializer else: # retrieve return PrintingJobDetailSerializer - + def get_queryset(self): """优化查询""" queryset = super().get_queryset() - if self.action in ['list', 'retrieve']: - queryset = queryset.select_related('printing_order', 'product') + if self.action in ["list", "retrieve"]: + queryset = queryset.select_related("printing_order", "product") # 预取批量推进记录(避免 serializer 产生 N+1) queryset = queryset.prefetch_related( Prefetch( - 'batch_advance_records', - queryset=models.PrintingJobBatchAdvanceRecord.objects.select_related('state', 'created_by') - .order_by('-created_at', '-id'), + "batch_advance_records", + queryset=models.PrintingJobBatchAdvanceRecord.objects.select_related( + "state", "created_by" + ).order_by("-created_at", "-id"), ) ) return queryset @@ -440,9 +505,9 @@ class PrintingJobViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): job_ids = [job.id for job in jobs] saleitems = list( - SalesItem.objects.select_related('shipment', 'created_by') + SalesItem.objects.select_related("shipment", "created_by") .filter(printing_job_id__in=job_ids) - .order_by('id') + .order_by("id") ) items_by_job_id = {job_id: [] for job_id in job_ids} @@ -475,17 +540,17 @@ class PrintingJobViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): def destroy(self, request, *args, **kwargs): """禁用删除操作""" return Response( - {'detail': '印染款式明细不支持删除操作'}, - status=status.HTTP_405_METHOD_NOT_ALLOWED + {"detail": "印染款式明细不支持删除操作"}, + status=status.HTTP_405_METHOD_NOT_ALLOWED, ) - - @action(detail=True, methods=['post'], url_path='advance-to-next-state') + + @action(detail=True, methods=["post"], url_path="advance-to-next-state") def advance_to_next_state(self, request, pk=None): """ 推进到下一个状态 - + 将印染任务推进到下一个流程状态 - + 请求体: { "parameters": { @@ -493,26 +558,27 @@ class PrintingJobViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): "operator": "张三" } } - + 参数可选,但如果下一个状态有必填参数,则必须提供 - + 注意:这是对 stateflow.BusinessObject.advance 接口的便捷包装, 自动通过 printing_job 查找关联的 business_object 并调用状态流转服务。 前端只需提供 printing_job 的 ID,无需关心 business_object_id。 """ job = self.get_object() - + if not job.business_object: return Response( - {'detail': '该任务没有关联的流程实例'}, - status=status.HTTP_400_BAD_REQUEST + {"detail": "该任务没有关联的流程实例"}, + status=status.HTTP_400_BAD_REQUEST, ) - + # 获取请求参数 - parameters = request.data.get('parameters', {}) - + parameters = request.data.get("parameters", {}) + # 调用 stateflow 统一服务进行状态流转 from stateflow import services as stateflow_services + # 修复历史/异常数据:确保该 job 的流程实例正确绑定到 job(避免 BusinessObject.object_id/content_type 为空) stateflow_services.ensure_business_object_bound_to_instance( job.business_object, @@ -522,202 +588,224 @@ class PrintingJobViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): success, message, state_log = stateflow_services.advance_to_next_state( job.business_object, request.user, **parameters ) - + if not success: - return Response( - {'detail': message}, - status=status.HTTP_400_BAD_REQUEST - ) - + return Response({"detail": message}, status=status.HTTP_400_BAD_REQUEST) + # 更新 updated_at 以使缓存失效 - job.save(update_fields=['updated_at']) - + job.save(update_fields=["updated_at"]) + # 重新获取 job 以刷新状态 job.refresh_from_db() serializer = PrintingJobDetailSerializer(job) - - return Response({ - 'detail': message, - 'data': serializer.data - }) - - @action(detail=True, methods=['post'], url_path='step-back-one-state') + + return Response({"detail": message, "data": serializer.data}) + + @action(detail=True, methods=["post"], url_path="step-back-one-state") def step_back_one_state(self, request, pk=None): """ 回退一步 - + 将印染任务回退到上一个流程状态 - + 注意:这是对 stateflow.BusinessObject.step_back 接口的便捷包装, 自动通过 printing_job 查找关联的 business_object 并调用状态流转服务。 前端只需提供 printing_job 的 ID,无需关心 business_object_id。 """ job = self.get_object() - + if not job.business_object: return Response( - {'detail': '该任务没有关联的流程实例'}, - status=status.HTTP_400_BAD_REQUEST + {"detail": "该任务没有关联的流程实例"}, + status=status.HTTP_400_BAD_REQUEST, ) - + # 调用 stateflow 统一服务进行状态回退 from stateflow import services as stateflow_services + success, message = stateflow_services.step_back_one_state( job.business_object, request.user ) - + if not success: - return Response( - {'detail': message}, - status=status.HTTP_400_BAD_REQUEST - ) - + return Response({"detail": message}, status=status.HTTP_400_BAD_REQUEST) + # 更新 updated_at 以使缓存失效 - job.save(update_fields=['updated_at']) - + job.save(update_fields=["updated_at"]) + # 重新获取 job 以刷新状态 job.refresh_from_db() serializer = PrintingJobDetailSerializer(job) - - return Response({ - 'detail': message, - 'data': serializer.data - }) - - @action(detail=True, methods=['get'], url_path='completed-states') + + return Response({"detail": message, "data": serializer.data}) + + @action(detail=True, methods=["get"], url_path="completed-states") def completed_states(self, request, pk=None): """ 查询已完成的流程列表 - + 查询参数: - include_cancelled: 是否包含已撤销的流程(true/false),默认 false """ job = self.get_object() - + if not job.business_object: - return Response({ - 'count': 0, - 'results': [] - }) - + return Response({"count": 0, "results": []}) + # 获取查询参数 - include_cancelled = request.query_params.get('include_cancelled', 'false').lower() == 'true' - + include_cancelled = ( + request.query_params.get("include_cancelled", "false").lower() == "true" + ) + # 查询已完成的状态 - state_logs = job.business_object.state_logs.select_related('state', 'completed_by').order_by('completed_at') - + state_logs = job.business_object.state_logs.select_related( + "state", "completed_by" + ).order_by("completed_at") + if not include_cancelled: state_logs = state_logs.filter(is_cancelled=False) - + results = [] for log in state_logs: - results.append({ - 'id': log.id, - 'state_id': log.state.id, - 'state_name': log.state.name, - 'completed_at': log.completed_at, - 'completed_by': log.completed_by.username if log.completed_by else None, - 'completed_by_name': getattr(log.completed_by, 'employee', None) and log.completed_by.employee.name or None, - 'is_cancelled': log.is_cancelled, - 'cancelled_at': log.cancelled_at, - }) - - return Response({ - 'count': len(results), - 'results': results - }) - - @action(detail=True, methods=['get'], url_path='timeline') + results.append( + { + "id": log.id, + "state_id": log.state.id, + "state_name": log.state.name, + "completed_at": log.completed_at, + "completed_by": log.completed_by.username + if log.completed_by + else None, + "completed_by_name": getattr(log.completed_by, "employee", None) + and log.completed_by.employee.name + or None, + "is_cancelled": log.is_cancelled, + "cancelled_at": log.cancelled_at, + } + ) + + return Response({"count": len(results), "results": results}) + + @action(detail=True, methods=["get"], url_path="timeline") def timeline(self, request, pk=None): """ 获取流程时间线 - + 返回该任务的完整流程时间线,包括未开始、进行中和已完成的状态 不包含已撤销的流程记录 """ job = self.get_object() - + if not job.business_object or not job.printing_order.process: - return Response({ - 'count': 0, - 'results': [] - }) - + return Response({"count": 0, "results": []}) + from stateflow import services as stateflow_services - timeline_data = stateflow_services.get_business_object_state_timeline(job.business_object) - + + timeline_data = stateflow_services.get_business_object_state_timeline( + job.business_object + ) + # 过滤掉已撤销的记录 - timeline_data = [item for item in timeline_data if not item.get('is_cancelled')] - + timeline_data = [item for item in timeline_data if not item.get("is_cancelled")] + results = [] for item in timeline_data: - results.append({ - 'state_id': item['state'].id, - 'state_name': item['state'].name, - 'state_description': item['state'].description, - 'order': item['order'], - 'status': item['status'], # not_started, in_progress, completed - 'completed_at': item['completed_at'], - 'completed_by': item['completed_by'].username if item['completed_by'] else None, - 'completed_by_name': getattr(item['completed_by'], 'employee', None) and item['completed_by'].employee.name or None if item['completed_by'] else None, - }) - - return Response({ - 'count': len(results), - 'results': results - }) + results.append( + { + "state_id": item["state"].id, + "state_name": item["state"].name, + "state_description": item["state"].description, + "order": item["order"], + "status": item["status"], # not_started, in_progress, completed + "completed_at": item["completed_at"], + "completed_by": item["completed_by"].username + if item["completed_by"] + else None, + "completed_by_name": getattr(item["completed_by"], "employee", None) + and item["completed_by"].employee.name + or None + if item["completed_by"] + else None, + } + ) + + return Response({"count": len(results), "results": results}) class HasInvalidatePlateOrderPermission(BasePermission): """自定义权限类,检查用户是否有作废开版订单的权限""" - message = '您没有权限作废开版订单' - + message = "您没有权限作废开版订单" + def has_permission(self, request, view): if not request.user.is_authenticated: return False - return request.user.has_perm('printing.can_invalidate_plateorder') + return request.user.has_perm("printing.can_invalidate_plateorder") class HasActivatePlateOrderPermission(BasePermission): """自定义权限类,检查用户是否有恢复开版订单的权限""" - message = '您没有权限恢复开版订单' - + message = "您没有权限恢复开版订单" + def has_permission(self, request, view): if not request.user.is_authenticated: return False - return request.user.has_perm('printing.can_activate_plateorder') + return request.user.has_perm("printing.can_activate_plateorder") class PlateOrderFilterSet(django_filters.FilterSet): """开版订单过滤器""" + MAX_RANGE_DAYS = 10 - customer_name = django_filters.CharFilter(field_name='customer__name', lookup_expr='icontains') - customer_phone = django_filters.CharFilter(field_name='customer__mobile', lookup_expr='icontains') - image_name = django_filters.CharFilter(lookup_expr='icontains') + customer_name = django_filters.CharFilter( + field_name="customer__name", lookup_expr="icontains" + ) + customer_phone = django_filters.CharFilter( + field_name="customer__mobile", lookup_expr="icontains" + ) + image_name = django_filters.CharFilter(lookup_expr="icontains") salesperson = django_filters.NumberFilter() merchandiser = django_filters.NumberFilter() - plate_type = django_filters.CharFilter(lookup_expr='icontains') - urgency_level = django_filters.CharFilter(lookup_expr='icontains') + plate_type = django_filters.CharFilter(lookup_expr="icontains") + urgency_level = django_filters.CharFilter(lookup_expr="icontains") is_invalid = django_filters.BooleanFilter() is_ordered = django_filters.BooleanFilter() is_mark_frame = django_filters.BooleanFilter() - fabric = django_filters.CharFilter(lookup_expr='icontains') - style_name = django_filters.CharFilter(lookup_expr='icontains') - plate_date_from = django_filters.DateFilter(field_name='plate_date', lookup_expr='gte') - plate_date_to = django_filters.DateFilter(field_name='plate_date', method='filter_plate_date_to') + fabric = django_filters.CharFilter(lookup_expr="icontains") + style_name = django_filters.CharFilter(lookup_expr="icontains") + plate_date_from = django_filters.DateFilter( + field_name="plate_date", lookup_expr="gte" + ) + plate_date_to = django_filters.DateFilter( + field_name="plate_date", method="filter_plate_date_to" + ) # plate_at_from / plate_at_to: 支持带时分秒的精确时间筛选(ISO 8601 格式) - plate_at_from = IsoDateTimeFilter(field_name='plate_date', lookup_expr='gte') - plate_at_to = IsoDateTimeFilter(field_name='plate_date', lookup_expr='lte') - required_completion_date_from = IsoDateTimeFilter(field_name='required_completion_date', lookup_expr='gte') - required_completion_date_to = IsoDateTimeFilter(field_name='required_completion_date', lookup_expr='lte') - created_date_from = django_filters.DateFilter(field_name='created_at', lookup_expr='gte') - created_date_to = django_filters.DateFilter(field_name='created_at', method='filter_created_date_to') - + plate_at_from = IsoDateTimeFilter(field_name="plate_date", lookup_expr="gte") + plate_at_to = IsoDateTimeFilter(field_name="plate_date", lookup_expr="lte") + required_completion_date_from = IsoDateTimeFilter( + field_name="required_completion_date", lookup_expr="gte" + ) + required_completion_date_to = IsoDateTimeFilter( + field_name="required_completion_date", lookup_expr="lte" + ) + created_date_from = django_filters.DateFilter( + field_name="created_at", lookup_expr="gte" + ) + created_date_to = django_filters.DateFilter( + field_name="created_at", method="filter_created_date_to" + ) + class Meta: model = models.PlateOrder - fields = ['customer', 'salesperson', 'merchandiser', 'urgency_level', 'is_invalid', 'is_ordered'] + fields = [ + "customer", + "salesperson", + "merchandiser", + "urgency_level", + "is_invalid", + "is_ordered", + ] def filter_plate_date_to(self, queryset, name, value): """ @@ -726,6 +814,7 @@ class PlateOrderFilterSet(django_filters.FilterSet): """ if value: from datetime import timedelta + next_day = value + timedelta(days=1) return queryset.filter(plate_date__lt=next_day) return queryset @@ -737,6 +826,7 @@ class PlateOrderFilterSet(django_filters.FilterSet): """ if value: from datetime import timedelta + next_day = value + timedelta(days=1) return queryset.filter(created_at__lt=next_day) return queryset @@ -745,7 +835,7 @@ class PlateOrderFilterSet(django_filters.FilterSet): def _to_local_date_from_datetime(raw_value): dt = parse_datetime(str(raw_value)) if dt is None: - raise DRFValidationError('日期时间格式无效') + raise DRFValidationError("日期时间格式无效") if timezone.is_aware(dt): dt = timezone.localtime(dt) return dt.date() @@ -754,7 +844,7 @@ class PlateOrderFilterSet(django_filters.FilterSet): def _to_date(raw_value): d = parse_date(str(raw_value)) if d is None: - raise DRFValidationError('日期格式无效') + raise DRFValidationError("日期格式无效") return d def _validate_range_days(self, from_key, to_key, mode, label): @@ -763,7 +853,7 @@ class PlateOrderFilterSet(django_filters.FilterSet): if not raw_from or not raw_to: return - if mode == 'datetime': + if mode == "datetime": from_date = self._to_local_date_from_datetime(raw_from) to_date = self._to_local_date_from_datetime(raw_to) else: @@ -772,19 +862,25 @@ class PlateOrderFilterSet(django_filters.FilterSet): days = (to_date - from_date).days + 1 if days > self.MAX_RANGE_DAYS: - raise DRFValidationError(f'{label}查询跨度不能超过{self.MAX_RANGE_DAYS}天') + raise DRFValidationError(f"{label}查询跨度不能超过{self.MAX_RANGE_DAYS}天") @property def qs(self): - self._validate_range_days('plate_date_from', 'plate_date_to', mode='date', label='开版日期') - self._validate_range_days('plate_at_from', 'plate_at_to', mode='datetime', label='开版时间') self._validate_range_days( - 'required_completion_date_from', - 'required_completion_date_to', - mode='datetime', - label='要求完成时间', + "plate_date_from", "plate_date_to", mode="date", label="开版日期" + ) + self._validate_range_days( + "plate_at_from", "plate_at_to", mode="datetime", label="开版时间" + ) + self._validate_range_days( + "required_completion_date_from", + "required_completion_date_to", + mode="datetime", + label="要求完成时间", + ) + self._validate_range_days( + "created_date_from", "created_date_to", mode="date", label="创建日期" ) - self._validate_range_days('created_date_from', 'created_date_to', mode='date', label='创建日期') return super().qs @@ -793,7 +889,11 @@ def _plate_order_last_modified(request, *args, **kwargs): 返回 PlateOrder 表中最新的 updated_at 时间。 用于条件请求(304 Not Modified)判断。 """ - latest = models.PlateOrder.objects.order_by('-updated_at').values_list('updated_at', flat=True).first() + latest = ( + models.PlateOrder.objects.order_by("-updated_at") + .values_list("updated_at", flat=True) + .first() + ) return latest @@ -801,9 +901,9 @@ def _plate_order_last_modified(request, *args, **kwargs): class PlateOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): """ 开版订单 ViewSet - + 提供开版订单的增改查功能(不支持删除) - + list: 获取开版订单列表 retrieve: 获取开版订单详情 create: 创建开版订单 @@ -816,12 +916,12 @@ class PlateOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): step_back_one_state: 回退一步(便捷包装) completed_states: 查询已完成的流程列表 timeline: 获取流程时间线 - + 注意:状态流转接口是对 stateflow 模块的便捷包装 - 前端只需提供 plate_order 的 ID - 系统会自动查找关联的 business_object 并进行状态流转 - 也可以直接使用 stateflow 的原始接口(需要 business_object_id) - + 查询参数: - customer: 客户ID - customer_name: 客户名称(模糊查询) @@ -845,17 +945,22 @@ class PlateOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): - created_date_to: 创建日期结束 - search: 全文搜索(设计编号、款式名称、客户名称、面料) - ordering: 排序字段 - + 权限控制: - 默认按客户可见性过滤(员工只能看到自己负责的客户的订单) - 拥有 printing.view_all_plateorders 权限可突破此限制 """ + queryset = models.PlateOrder.objects.all() - customer_field = 'customer' - view_all_permission = 'printing.view_all_plateorders' + customer_field = "customer" + view_all_permission = "printing.view_all_plateorders" permission_classes = [DjangoModelPermissions] parser_classes = [MultiPartParser, FormParser, JSONParser] # 支持文件上传 - filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + filters.OrderingFilter, + ] filterset_class = PlateOrderFilterSet # search_fields 说明(模糊搜索支持的字段): # - design_code: 设计编号 @@ -869,16 +974,29 @@ class PlateOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): # - default_address: 默认地址 # 待添加:area, customer_feedback, fabric_source, width 等 search_fields = [ - 'design_code', 'design_code_normalized', 'style_name', 'customer__name', 'fabric', - 'plate_notes', 'reprint_reason', 'image_name', 'default_address', + "design_code", + "design_code_normalized", + "style_name", + "customer__name", + "fabric", + "plate_notes", + "reprint_reason", + "image_name", + "default_address", ] ordering_fields = [ - 'id', 'created_at', 'updated_at', 'plate_date', - 'required_completion_date', 'completion_date', - 'urgency_level', 'is_ordered', 'is_invalid' + "id", + "created_at", + "updated_at", + "plate_date", + "required_completion_date", + "completion_date", + "urgency_level", + "is_ordered", + "is_invalid", ] - ordering = ['-created_at'] - + ordering = ["-created_at"] + def get_serializer_class(self): """根据动作选择序列化器""" from .serializers import ( @@ -886,36 +1004,44 @@ class PlateOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): PlateOrderDetailSerializer, PlateOrderCreateUpdateSerializer, ) - - if self.action == 'list': + + if self.action == "list": return PlateOrderListSerializer - elif self.action in ['create', 'update', 'partial_update']: + elif self.action in ["create", "update", "partial_update"]: return PlateOrderCreateUpdateSerializer else: # retrieve return PlateOrderDetailSerializer - + def get_queryset(self): """优化查询""" queryset = super().get_queryset() # 默认不返回作废订单(仅 list);如显式传入 is_invalid 参数,则交由 FilterSet 处理 # 注意:不能影响 activate/invalidate 等 action,否则会因 get_object() 被过滤而 404。 - if self.action == 'list' and self.request and 'is_invalid' not in self.request.query_params: + if ( + self.action == "list" + and self.request + and "is_invalid" not in self.request.query_params + ): queryset = queryset.filter(is_invalid=False) - if self.action in ['list', 'retrieve']: - queryset = queryset.select_related('customer', 'salesperson', 'merchandiser', 'business_object') + if self.action in ["list", "retrieve"]: + queryset = queryset.select_related( + "customer", "salesperson", "merchandiser", "business_object" + ) # 为搜索提供 design_code 的兜底(为空时使用主键字符串) queryset = queryset.annotate( - design_code_normalized=Coalesce('design_code', Cast('id', output_field=CharField())) + design_code_normalized=Coalesce( + "design_code", Cast("id", output_field=CharField()) + ) ) return queryset - + def destroy(self, request, *args, **kwargs): """禁用删除操作""" return Response( - {'detail': '开版订单不支持删除操作,请使用作废功能'}, - status=status.HTTP_405_METHOD_NOT_ALLOWED + {"detail": "开版订单不支持删除操作,请使用作废功能"}, + status=status.HTTP_405_METHOD_NOT_ALLOWED, ) def perform_create(self, serializer): @@ -924,81 +1050,78 @@ class PlateOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): """ user = self.request.user merchant = None - if hasattr(user, 'employee') and user.employee and user.employee.merchant: + if hasattr(user, "employee") and user.employee and user.employee.merchant: merchant = user.employee.merchant serializer.save(created_by=user, merchant=merchant) - - @action(detail=True, methods=['post']) + + @action(detail=True, methods=["post"]) def invalidate(self, request, pk=None): """ 作废开版订单 - + 需要权限: printing.can_invalidate_plateorder """ # 检查权限 - if not request.user.has_perm('printing.can_invalidate_plateorder'): + if not request.user.has_perm("printing.can_invalidate_plateorder"): return Response( - {'detail': '您没有权限作废开版订单'}, - status=status.HTTP_403_FORBIDDEN + {"detail": "您没有权限作废开版订单"}, status=status.HTTP_403_FORBIDDEN ) - + plate_order = self.get_object() - + if plate_order.is_invalid: return Response( - {'detail': '该开版订单已经作废'}, - status=status.HTTP_400_BAD_REQUEST + {"detail": "该开版订单已经作废"}, status=status.HTTP_400_BAD_REQUEST ) - + plate_order.is_invalid = True plate_order.save() - + from .serializers import PlateOrderDetailSerializer - serializer = PlateOrderDetailSerializer(plate_order, context={'request': request}) - return Response({ - 'detail': '开版订单已作废', - 'data': serializer.data - }) - - @action(detail=True, methods=['post']) + + serializer = PlateOrderDetailSerializer( + plate_order, context={"request": request} + ) + return Response({"detail": "开版订单已作废", "data": serializer.data}) + + @action(detail=True, methods=["post"]) def activate(self, request, pk=None): """ 恢复开版订单 - + 需要权限: printing.can_activate_plateorder """ # 检查权限 - if not request.user.has_perm('printing.can_activate_plateorder'): + if not request.user.has_perm("printing.can_activate_plateorder"): return Response( - {'detail': '您没有权限恢复开版订单'}, - status=status.HTTP_403_FORBIDDEN + {"detail": "您没有权限恢复开版订单"}, status=status.HTTP_403_FORBIDDEN ) - + plate_order = self.get_object() - + if not plate_order.is_invalid: return Response( - {'detail': '该开版订单未作废,无需恢复'}, - status=status.HTTP_400_BAD_REQUEST + {"detail": "该开版订单未作废,无需恢复"}, + status=status.HTTP_400_BAD_REQUEST, ) - + plate_order.is_invalid = False plate_order.save() - + from .serializers import PlateOrderDetailSerializer - serializer = PlateOrderDetailSerializer(plate_order, context={'request': request}) - return Response({ - 'detail': '开版订单已恢复', - 'data': serializer.data - }) - - @action(detail=True, methods=['post'], url_path='advance-to-next-state') + + serializer = PlateOrderDetailSerializer( + plate_order, context={"request": request} + ) + return Response({"detail": "开版订单已恢复", "data": serializer.data}) + + @action(detail=True, methods=["post"], url_path="advance-to-next-state") def advance_to_next_state(self, request, pk=None): """ 推进到下一个状态 - + 将开版订单推进到下一个流程状态 - + 请求体: { "parameters": { @@ -1006,25 +1129,26 @@ class PlateOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): "quality_score": "95" } } - + 参数可选,但如果下一个状态有必填参数,则必须提供 - + 注意:这是对 stateflow.BusinessObject.advance 接口的便捷包装, 自动通过 plate_order 查找关联的 business_object 并调用状态流转服务。 """ plate_order = self.get_object() - + if not plate_order.business_object: return Response( - {'detail': '该开版订单没有关联的流程实例'}, - status=status.HTTP_400_BAD_REQUEST + {"detail": "该开版订单没有关联的流程实例"}, + status=status.HTTP_400_BAD_REQUEST, ) - + # 获取请求参数 - parameters = request.data.get('parameters', {}) - + parameters = request.data.get("parameters", {}) + # 调用 stateflow 统一服务进行状态流转 from stateflow import services as stateflow_services + # 修复历史/异常数据:确保该 plate_order 的流程实例正确绑定到 plate_order(避免 BusinessObject.object_id/content_type 为空) stateflow_services.ensure_business_object_bound_to_instance( plate_order.business_object, @@ -1034,159 +1158,160 @@ class PlateOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): success, message, state_log = stateflow_services.advance_to_next_state( plate_order.business_object, request.user, **parameters ) - + if not success: - return Response( - {'detail': message}, - status=status.HTTP_400_BAD_REQUEST - ) - + return Response({"detail": message}, status=status.HTTP_400_BAD_REQUEST) + # 更新 updated_at 以使缓存失效 - plate_order.save(update_fields=['updated_at']) - + plate_order.save(update_fields=["updated_at"]) + # 重新获取 plate_order 以刷新状态 plate_order.refresh_from_db() from .serializers import PlateOrderDetailSerializer - serializer = PlateOrderDetailSerializer(plate_order, context={'request': request}) - - return Response({ - 'detail': message, - 'data': serializer.data - }) - - @action(detail=True, methods=['post'], url_path='step-back-one-state') + + serializer = PlateOrderDetailSerializer( + plate_order, context={"request": request} + ) + + return Response({"detail": message, "data": serializer.data}) + + @action(detail=True, methods=["post"], url_path="step-back-one-state") def step_back_one_state(self, request, pk=None): """ 回退一步 - + 将开版订单回退到上一个流程状态 - + 注意:这是对 stateflow.BusinessObject.step_back 接口的便捷包装, 自动通过 plate_order 查找关联的 business_object 并调用状态流转服务。 """ plate_order = self.get_object() - + if not plate_order.business_object: return Response( - {'detail': '该开版订单没有关联的流程实例'}, - status=status.HTTP_400_BAD_REQUEST + {"detail": "该开版订单没有关联的流程实例"}, + status=status.HTTP_400_BAD_REQUEST, ) - + # 调用 stateflow 统一服务进行状态回退 from stateflow import services as stateflow_services + success, message = stateflow_services.step_back_one_state( plate_order.business_object, request.user ) - + if not success: - return Response( - {'detail': message}, - status=status.HTTP_400_BAD_REQUEST - ) - + return Response({"detail": message}, status=status.HTTP_400_BAD_REQUEST) + # 更新 updated_at 以使缓存失效 - plate_order.save(update_fields=['updated_at']) - + plate_order.save(update_fields=["updated_at"]) + # 重新获取 plate_order 以刷新状态 plate_order.refresh_from_db() from .serializers import PlateOrderDetailSerializer - serializer = PlateOrderDetailSerializer(plate_order, context={'request': request}) - - return Response({ - 'detail': message, - 'data': serializer.data - }) - - @action(detail=True, methods=['get'], url_path='completed-states') + + serializer = PlateOrderDetailSerializer( + plate_order, context={"request": request} + ) + + return Response({"detail": message, "data": serializer.data}) + + @action(detail=True, methods=["get"], url_path="completed-states") def completed_states(self, request, pk=None): """ 查询已完成的流程列表 - + 查询参数: - include_cancelled: 是否包含已撤销的流程(true/false),默认 false """ plate_order = self.get_object() - + if not plate_order.business_object: - return Response({ - 'count': 0, - 'results': [] - }) - + return Response({"count": 0, "results": []}) + # 获取查询参数 - include_cancelled = request.query_params.get('include_cancelled', 'false').lower() == 'true' - + include_cancelled = ( + request.query_params.get("include_cancelled", "false").lower() == "true" + ) + # 查询已完成的状态 - state_logs = plate_order.business_object.state_logs.select_related('state', 'completed_by').order_by('completed_at') - + state_logs = plate_order.business_object.state_logs.select_related( + "state", "completed_by" + ).order_by("completed_at") + if not include_cancelled: state_logs = state_logs.filter(is_cancelled=False) - + results = [] for log in state_logs: - results.append({ - 'id': log.id, - 'state_id': log.state.id, - 'state_name': log.state.name, - 'completed_at': log.completed_at, - 'completed_by': log.completed_by.username if log.completed_by else None, - 'completed_by_name': getattr(log.completed_by, 'employee', None) and log.completed_by.employee.name or None, - 'is_cancelled': log.is_cancelled, - 'cancelled_at': log.cancelled_at, - }) - - return Response({ - 'count': len(results), - 'results': results - }) - - @action(detail=True, methods=['get'], url_path='timeline') + results.append( + { + "id": log.id, + "state_id": log.state.id, + "state_name": log.state.name, + "completed_at": log.completed_at, + "completed_by": log.completed_by.username + if log.completed_by + else None, + "completed_by_name": getattr(log.completed_by, "employee", None) + and log.completed_by.employee.name + or None, + "is_cancelled": log.is_cancelled, + "cancelled_at": log.cancelled_at, + } + ) + + return Response({"count": len(results), "results": results}) + + @action(detail=True, methods=["get"], url_path="timeline") def timeline(self, request, pk=None): """ 获取流程时间线 - + 返回该开版订单的完整流程时间线,包括未开始、进行中和已完成的状态 不包含已撤销的流程记录 """ plate_order = self.get_object() - + if not plate_order.business_object: - return Response({ - 'count': 0, - 'results': [] - }) - + return Response({"count": 0, "results": []}) + from stateflow import services as stateflow_services - + # 获取流程 from stateflow.models import Process + try: process = Process.objects.get(id=plate_order.process) except Process.DoesNotExist: - return Response({ - 'count': 0, - 'results': [] - }) - - timeline_data = stateflow_services.get_business_object_state_timeline(plate_order.business_object) - + return Response({"count": 0, "results": []}) + + timeline_data = stateflow_services.get_business_object_state_timeline( + plate_order.business_object + ) + # 过滤掉已撤销的记录 - timeline_data = [item for item in timeline_data if not item.get('is_cancelled')] - + timeline_data = [item for item in timeline_data if not item.get("is_cancelled")] + results = [] for item in timeline_data: - results.append({ - 'state_id': item['state'].id, - 'state_name': item['state'].name, - 'state_description': item['state'].description, - 'order': item['order'], - 'status': item['status'], # not_started, in_progress, completed - 'completed_at': item['completed_at'], - 'completed_by': item['completed_by'].username if item['completed_by'] else None, - 'completed_by_name': getattr(item['completed_by'], 'employee', None) and item['completed_by'].employee.name or None if item['completed_by'] else None, - }) - - return Response({ - 'count': len(results), - 'results': results - }) + results.append( + { + "state_id": item["state"].id, + "state_name": item["state"].name, + "state_description": item["state"].description, + "order": item["order"], + "status": item["status"], # not_started, in_progress, completed + "completed_at": item["completed_at"], + "completed_by": item["completed_by"].username + if item["completed_by"] + else None, + "completed_by_name": getattr(item["completed_by"], "employee", None) + and item["completed_by"].employee.name + or None + if item["completed_by"] + else None, + } + ) + + return Response({"count": len(results), "results": results})