1
0
forked from erp-dev/erp
Files
erpnew/api_v1/views/printing/views.py
2026-01-12 13:15:41 +08:00

1009 lines
39 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.
"""
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: 排序字段
返回字段说明:
- jobs_status_summary: 订单下所有印染任务按下一待执行状态分组的数量汇总
格式: [{"state_name": "进度一", "state_id": 1, "count": 3}, ...]
- jobs_last_status_summary: 订单下所有印染任务按最后完成状态分组的数量汇总
格式: [{"state_name": "进度一", "count": 3}, ...]
"""
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')
# 预取 printing_jobs 及其 business_object用于状态汇总统计
# 同时预取 state_logs 和 process 相关数据以减少 job.status 属性调用时的 N+1 查询
queryset = queryset.prefetch_related(
Prefetch(
'printing_jobs',
queryset=models.PrintingJob.objects.select_related(
'business_object',
'business_object__process',
).prefetch_related(
'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
)
@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')
# 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')
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: 开版日期结束(仅日期,包含整天)
- plate_at_from: 开版时间起始ISO 8601 格式,含时分秒,如 2024-01-01T08:00:00
- plate_at_to: 开版时间结束ISO 8601 格式,含时分秒)
- 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
# search_fields 说明(模糊搜索支持的字段):
# - design_code: 设计编号
# - design_code_normalized: 设计编号兜底(为空时使用主键 ID通过 get_queryset 注解)
# - style_name: 款号名称
# - customer__name: 客户名称(跨表)
# - fabric: 布料
# - plate_notes: 打版注意事项
# - reprint_reason: 复版原因
# - image_name: 图片名称
# - 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',
]
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
})