""" Printing API ViewSet """ from rest_framework import viewsets, filters, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import BasePermission from rest_framework.permissions import DjangoModelPermissions from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from django.db.models import CharField, Prefetch from django.db.models.functions import Cast, Coalesce from django.views.decorators.cache import cache_page from django.views.decorators.http import condition from django.utils.decorators import method_decorator from django_filters.rest_framework import DjangoFilterBackend from django_filters import rest_framework as django_filters from django_filters import IsoDateTimeFilter from flower.viewsets import LimitedModelViewSet from printing import models from basic_info.models import MerchantTypeEnum from .serializers import ( PrintingOrderListSerializer, PrintingOrderDetailSerializer, PrintingOrderCreateUpdateSerializer, PrintingJobListSerializer, PrintingJobDetailSerializer, PrintingJobCreateUpdateSerializer, ) class IsPrintingFactory(BasePermission): """自定义权限类,允许印染工厂用户访问""" message = '您没有访问印染订单的权限' def has_permission(self, request, view): if not request.user.is_authenticated: return False if hasattr(request.user, 'employee'): return request.user.employee.merchant.type == MerchantTypeEnum.FACTORY return False class HasInvalidatePrintingOrderPermission(BasePermission): """自定义权限类,检查用户是否有作废印染订单的权限""" message = '您没有权限作废订单' def has_permission(self, request, view): if not request.user.is_authenticated: return False return request.user.has_perm('printing.can_invalidate_printingorder') class HasActivatePrintingOrderPermission(BasePermission): """自定义权限类,检查用户是否有恢复印染订单的权限""" message = '您没有权限恢复订单' def has_permission(self, request, view): if not request.user.is_authenticated: return False return request.user.has_perm('printing.can_activate_printingorder') class PrintingOrderFilterSet(django_filters.FilterSet): """印染订单过滤器""" 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.DateFilter(field_name='outgoing_date', lookup_expr='gte') outgoing_date_to = django_filters.DateFilter(field_name='outgoing_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.PrintingOrder fields = ['customer', 'is_urgent', 'is_fabric_received', 'is_invalid'] def filter_created_date_to(self, queryset, name, value): """ 过滤 created_at <= 指定日期的 23:59:59.999999 使用 lt 次日 00:00:00 实现,确保包含整天 """ 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') class PrintingOrderViewSet(LimitedModelViewSet): """ 印染订单 ViewSet 提供印染订单的增改查功能(不支持删除) list: 获取印染订单列表 retrieve: 获取印染订单详情 create: 创建印染订单 update: 更新印染订单 partial_update: 部分更新印染订单 查询参数: - customer: 客户ID - customer_name: 客户名称(模糊查询) - customer_phone: 客户电话(模糊查询) - fabric: 面料(模糊查询) - is_urgent: 是否紧急(true/false) - is_fabric_received: 布料是否已收(true/false) - is_invalid: 是否作废(true/false) - area: 地区(模糊查询) - outgoing_date_from: 出货日期起始 - outgoing_date_to: 出货日期结束 - created_date_from: 创建日期起始 - created_date_to: 创建日期结束 - search: 全文搜索(客户名称、面料、地区、工艺) - ordering: 排序字段 """ queryset = models.PrintingOrder.objects.all() 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' ] ordering = ['-created_at'] def get_permissions(self): permissions = super().get_permissions() + [IsPrintingFactory()] if self.action == 'invalidate': permissions.append(HasInvalidatePrintingOrderPermission()) elif self.action == 'activate': permissions.append(HasActivatePrintingOrderPermission()) return permissions def get_serializer_class(self): """根据动作选择序列化器""" if self.action == 'list': return PrintingOrderListSerializer elif self.action in ['create', 'update', 'partial_update']: return PrintingOrderCreateUpdateSerializer else: # retrieve return PrintingOrderDetailSerializer def get_queryset(self): """优化查询""" queryset = super().get_queryset() if self.action in ['list', 'retrieve']: queryset = queryset.select_related('customer') return queryset def destroy(self, request, *args, **kwargs): """禁用删除操作""" return Response( {'detail': '印染订单不支持删除操作,请使用作废功能'}, status=status.HTTP_405_METHOD_NOT_ALLOWED ) @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'): return Response( {'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 ) printing_order.is_invalid = True printing_order.save() serializer = PrintingOrderDetailSerializer(printing_order) 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'): return Response( {'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 ) printing_order.is_invalid = False printing_order.save() serializer = PrintingOrderDetailSerializer(printing_order) 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 ) printing_order.is_fabric_received = True printing_order.save() serializer = PrintingOrderDetailSerializer(printing_order) return Response({ 'detail': '已标记布料已收', 'data': serializer.data }) class PrintingJobFilterSet(django_filters.FilterSet): """印染款式明细过滤器""" printing_order = django_filters.NumberFilter() 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') class Meta: model = models.PrintingJob fields = ['printing_order', 'product'] class PrintingJobViewSet(LimitedModelViewSet): """ 印染款式明细 ViewSet 提供印染款式明细的增改查功能(不支持删除) list: 获取款式明细列表 retrieve: 获取款式明细详情 create: 创建款式明细 update: 更新款式明细 partial_update: 部分更新款式明细 advance_to_next_state: 推进到下一个状态 step_back_one_state: 回退一步 completed_states: 查询已完成的流程列表 timeline: 获取流程时间线 注意:状态流转接口是对 stateflow 模块的便捷包装 - 前端只需提供 printing_job 的 ID - 系统会自动查找关联的 business_object 并进行状态流转 - 也可以直接使用 stateflow 的原始接口(需要 business_object_id) 查询参数: - printing_order: 印染订单ID - product: 产品ID - product_name: 产品名称(模糊查询) - unit: 单位(模糊查询) - quantity_min: 最小数量 - quantity_max: 最大数量 - pieces_min: 最小件数 - pieces_max: 最大件数 - search: 全文搜索(产品名称、单位、尺寸、备注) - ordering: 排序字段 """ queryset = models.PrintingJob.objects.all() permission_classes = [DjangoModelPermissions, IsPrintingFactory] 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'] def get_serializer_class(self): """根据动作选择序列化器""" if self.action == 'list': return PrintingJobListSerializer 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') # 预取批量推进记录(避免 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'), ) ) return queryset def destroy(self, request, *args, **kwargs): """禁用删除操作""" return Response( {'detail': '印染款式明细不支持删除操作'}, status=status.HTTP_405_METHOD_NOT_ALLOWED ) @action(detail=True, methods=['post'], url_path='advance-to-next-state') def advance_to_next_state(self, request, pk=None): """ 推进到下一个状态 将印染任务推进到下一个流程状态 请求体: { "parameters": { "temperature": "25.5", "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 ) # 获取请求参数 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, job, default_name=f"PrintingJob-{job.id}", ) 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 ) # 更新 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') 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 ) # 调用 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 ) # 更新 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') 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': [] }) # 获取查询参数 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') 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') 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': [] }) from stateflow import services as stateflow_services 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')] 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 }) class HasInvalidatePlateOrderPermission(BasePermission): """自定义权限类,检查用户是否有作废开版订单的权限""" message = '您没有权限作废开版订单' def has_permission(self, request, view): if not request.user.is_authenticated: return False return request.user.has_perm('printing.can_invalidate_plateorder') class HasActivatePlateOrderPermission(BasePermission): """自定义权限类,检查用户是否有恢复开版订单的权限""" message = '您没有权限恢复开版订单' def has_permission(self, request, view): if not request.user.is_authenticated: return False return request.user.has_perm('printing.can_activate_plateorder') class PlateOrderFilterSet(django_filters.FilterSet): """开版订单过滤器""" 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') 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') 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'] def filter_plate_date_to(self, queryset, name, value): """ 过滤 plate_date <= 指定日期的 23:59:59.999999 使用 lt 次日 00:00:00 实现,确保包含整天 """ if value: from datetime import timedelta next_day = value + timedelta(days=1) return queryset.filter(plate_date__lt=next_day) return queryset def filter_created_date_to(self, queryset, name, value): """ 过滤 created_at <= 指定日期的 23:59:59.999999 使用 lt 次日 00:00:00 实现,确保包含整天 """ if value: from datetime import timedelta next_day = value + timedelta(days=1) return queryset.filter(created_at__lt=next_day) return queryset 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() return latest # @method_decorator(condition(last_modified_func=_plate_order_last_modified), name='list') class PlateOrderViewSet(LimitedModelViewSet): """ 开版订单 ViewSet 提供开版订单的增改查功能(不支持删除) list: 获取开版订单列表 retrieve: 获取开版订单详情 create: 创建开版订单 image_name: 图片名称(模糊查询) update: 更新开版订单 partial_update: 部分更新开版订单 invalidate: 作废开版订单 activate: 恢复开版订单 advance_to_next_state: 推进到下一个状态(便捷包装) step_back_one_state: 回退一步(便捷包装) completed_states: 查询已完成的流程列表 timeline: 获取流程时间线 注意:状态流转接口是对 stateflow 模块的便捷包装 - 前端只需提供 plate_order 的 ID - 系统会自动查找关联的 business_object 并进行状态流转 - 也可以直接使用 stateflow 的原始接口(需要 business_object_id) 查询参数: - customer: 客户ID - customer_name: 客户名称(模糊查询) - customer_phone: 客户电话(模糊查询) - salesperson: 业务员ID - merchandiser: 跟单员ID - plate_type: 版型(模糊查询) - urgency_level: 紧急程度(1-5) - is_invalid: 是否作废(true/false) - is_ordered: 是否已下单(true/false) - is_mark_frame: 是否打样架(true/false) - fabric: 面料(模糊查询) - style_name: 款式名称(模糊查询) - plate_date_from: 开版日期起始 - plate_date_to: 开版日期结束 - required_completion_date_from: 要求完成时间起始(含时分秒) - required_completion_date_to: 要求完成时间结束 - created_date_from: 创建日期起始 - created_date_to: 创建日期结束 - search: 全文搜索(设计编号、款式名称、客户名称、面料) - ordering: 排序字段 """ queryset = models.PlateOrder.objects.all() permission_classes = [DjangoModelPermissions] parser_classes = [MultiPartParser, FormParser, JSONParser] # 支持文件上传 filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_class = PlateOrderFilterSet # design_code_normalized: 当 design_code 为空时用 id 兜底,便于搜索数字编号 search_fields = ['design_code', 'design_code_normalized', 'style_name', 'customer__name', 'fabric'] ordering_fields = [ 'id', 'created_at', 'updated_at', 'plate_date', 'required_completion_date', 'completion_date', 'urgency_level', 'is_ordered', 'is_invalid' ] ordering = ['-created_at'] def get_serializer_class(self): """根据动作选择序列化器""" from .serializers import ( PlateOrderListSerializer, PlateOrderDetailSerializer, PlateOrderCreateUpdateSerializer, ) if self.action == 'list': return PlateOrderListSerializer elif self.action in ['create', 'update', 'partial_update']: return PlateOrderCreateUpdateSerializer else: # retrieve return PlateOrderDetailSerializer def get_queryset(self): """优化查询""" queryset = super().get_queryset() 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())) ) return queryset def destroy(self, request, *args, **kwargs): """禁用删除操作""" return Response( {'detail': '开版订单不支持删除操作,请使用作废功能'}, status=status.HTTP_405_METHOD_NOT_ALLOWED ) def perform_create(self, serializer): """ 创建时自动绑定创建人(created_by),不允许前端传参控制。 """ serializer.save(created_by=self.request.user) @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'): return Response( {'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 ) 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']) def activate(self, request, pk=None): """ 恢复开版订单 需要权限: printing.can_activate_plateorder """ # 检查权限 if not request.user.has_perm('printing.can_activate_plateorder'): return Response( {'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 ) 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') def advance_to_next_state(self, request, pk=None): """ 推进到下一个状态 将开版订单推进到下一个流程状态 请求体: { "parameters": { "inspector": "李四", "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 ) # 获取请求参数 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, plate_order, default_name=f"PlateOrder-{plate_order.id}", ) 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 ) # 更新 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') 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 ) # 调用 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 ) # 更新 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') 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': [] }) # 获取查询参数 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') 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') def timeline(self, request, pk=None): """ 获取流程时间线 返回该开版订单的完整流程时间线,包括未开始、进行中和已完成的状态 不包含已撤销的流程记录 """ plate_order = self.get_object() if not plate_order.business_object: 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) # 过滤掉已撤销的记录 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 })