forked from erp-dev/erp
1318 lines
47 KiB
Python
1318 lines
47 KiB
Python
"""
|
||
Printing API ViewSet
|
||
"""
|
||
|
||
from rest_framework import viewsets, filters, status
|
||
from rest_framework.decorators import action
|
||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||
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.core.exceptions import ValidationError
|
||
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.utils import timezone
|
||
from django.utils.dateparse import parse_date, parse_datetime
|
||
from django_filters.rest_framework import DjangoFilterBackend
|
||
from django_filters import rest_framework as django_filters
|
||
from django_filters import IsoDateTimeFilter
|
||
from datetime import datetime, time, timedelta
|
||
|
||
from flower.viewsets import LimitedModelViewSet
|
||
from printing import models
|
||
from basic_info.models import MerchantTypeEnum
|
||
|
||
from .serializers import (
|
||
PrintingOrderListSerializer,
|
||
PrintingOrderDetailSerializer,
|
||
PrintingOrderCreateUpdateSerializer,
|
||
PrintingJobListSerializer,
|
||
PrintingJobDetailSerializer,
|
||
PrintingJobCreateUpdateSerializer,
|
||
)
|
||
from .mixins import CustomerVisibilityFilterMixin
|
||
|
||
|
||
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):
|
||
"""印染订单过滤器"""
|
||
|
||
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"
|
||
)
|
||
# 支持同时搜索 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",
|
||
"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]:
|
||
"""
|
||
解析日期/日期时间字符串为“当前时区”的 aware datetime。
|
||
|
||
返回:
|
||
- (dt, has_time)
|
||
- has_time=True:输入包含时间(datetime)
|
||
- has_time=False:输入为纯日期(date-only)
|
||
"""
|
||
s = (value or "").strip()
|
||
if not s:
|
||
return None, False
|
||
|
||
tz = timezone.get_current_timezone()
|
||
|
||
# 注意:Django 的 parse_datetime 在某些版本会把纯日期(YYYY-MM-DD)也解析成 00:00:00,
|
||
# 这会让“to=日期”语义从“包含整天”退化为“<= 当天 00:00:00”。因此这里先尝试 parse_date。
|
||
d = parse_date(s)
|
||
if d is not None:
|
||
dt0 = timezone.make_aware(datetime.combine(d, time.min), tz)
|
||
return dt0, False
|
||
|
||
dt = parse_datetime(s)
|
||
if dt is not None:
|
||
if timezone.is_naive(dt):
|
||
dt = timezone.make_aware(dt, tz)
|
||
else:
|
||
dt = timezone.localtime(dt, tz)
|
||
return dt, True
|
||
|
||
return None, False
|
||
|
||
def filter_outgoing_date_from(self, queryset, name, value):
|
||
dt, _has_time = self._parse_date_or_datetime(value)
|
||
if dt is None:
|
||
# 与 DRF/DjangoFilterBackend 行为保持一致:参数非法返回 400
|
||
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 必须是日期或日期时间")
|
||
|
||
# 纯日期:包含整天 => outgoing_date < 次日 00:00:00
|
||
if not has_time:
|
||
return queryset.filter(outgoing_date__lt=dt + timedelta(days=1))
|
||
|
||
# 日期时间:按“<=”包含到秒
|
||
return queryset.filter(outgoing_date__lte=dt)
|
||
|
||
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(CustomerVisibilityFilterMixin, LimitedModelViewSet):
|
||
"""
|
||
印染订单 ViewSet
|
||
|
||
提供印染订单的增改查功能(不支持删除)
|
||
|
||
list: 获取印染订单列表
|
||
retrieve: 获取印染订单详情
|
||
create: 创建印染订单
|
||
update: 更新印染订单
|
||
partial_update: 部分更新印染订单
|
||
|
||
查询参数:
|
||
- customer: 客户ID
|
||
- external_order_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}, ...]
|
||
|
||
权限控制:
|
||
- 默认按客户可见性过滤(员工只能看到自己负责的客户的订单)
|
||
- 拥有 printing.view_all_printingorders 权限可突破此限制
|
||
"""
|
||
|
||
queryset = models.PrintingOrder.objects.all()
|
||
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",
|
||
]
|
||
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()
|
||
|
||
# 默认不返回作废订单(仅 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
|
||
):
|
||
queryset = queryset.filter(is_invalid=False)
|
||
|
||
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()
|
||
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")
|
||
|
||
class Meta:
|
||
model = models.PrintingJob
|
||
fields = ["printing_order", "product", "external_order_id"]
|
||
|
||
|
||
class PrintingJobViewSet(CustomerVisibilityFilterMixin, 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
|
||
- external_order_id: 外部订单编号(按所属订单模糊查询)
|
||
- product: 产品ID
|
||
- product_name: 产品名称(模糊查询)
|
||
- unit: 单位(模糊查询)
|
||
- quantity_min: 最小数量
|
||
- quantity_max: 最大数量
|
||
- pieces_min: 最小件数
|
||
- 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"
|
||
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
|
||
|
||
@staticmethod
|
||
def _attach_saleitems_cache(jobs):
|
||
if not jobs:
|
||
return
|
||
|
||
from shipment.models import SalesItem
|
||
|
||
job_ids = [job.id for job in jobs]
|
||
saleitems = list(
|
||
SalesItem.objects.select_related("shipment", "created_by")
|
||
.filter(printing_job_id__in=job_ids)
|
||
.order_by("id")
|
||
)
|
||
|
||
items_by_job_id = {job_id: [] for job_id in job_ids}
|
||
for item in saleitems:
|
||
items_by_job_id.setdefault(item.printing_job_id, []).append(item)
|
||
|
||
for job in jobs:
|
||
job._saleitems_cache = items_by_job_id.get(job.id, [])
|
||
|
||
def list(self, request, *args, **kwargs):
|
||
queryset = self.filter_queryset(self.get_queryset())
|
||
|
||
page = self.paginate_queryset(queryset)
|
||
if page is not None:
|
||
self._attach_saleitems_cache(page)
|
||
serializer = self.get_serializer(page, many=True)
|
||
return self.get_paginated_response(serializer.data)
|
||
|
||
jobs = list(queryset)
|
||
self._attach_saleitems_cache(jobs)
|
||
serializer = self.get_serializer(jobs, many=True)
|
||
return Response(serializer.data)
|
||
|
||
def retrieve(self, request, *args, **kwargs):
|
||
instance = self.get_object()
|
||
self._attach_saleitems_cache([instance])
|
||
serializer = self.get_serializer(instance)
|
||
return Response(serializer.data)
|
||
|
||
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):
|
||
"""开版订单过滤器"""
|
||
|
||
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")
|
||
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
|
||
|
||
@staticmethod
|
||
def _to_local_date_from_datetime(raw_value):
|
||
dt = parse_datetime(str(raw_value))
|
||
if dt is None:
|
||
raise DRFValidationError("日期时间格式无效")
|
||
if timezone.is_aware(dt):
|
||
dt = timezone.localtime(dt)
|
||
return dt.date()
|
||
|
||
@staticmethod
|
||
def _to_date(raw_value):
|
||
d = parse_date(str(raw_value))
|
||
if d is None:
|
||
raise DRFValidationError("日期格式无效")
|
||
return d
|
||
|
||
def _validate_range_days(self, from_key, to_key, mode, label):
|
||
raw_from = self.data.get(from_key)
|
||
raw_to = self.data.get(to_key)
|
||
if not raw_from or not raw_to:
|
||
return
|
||
|
||
if mode == "datetime":
|
||
from_date = self._to_local_date_from_datetime(raw_from)
|
||
to_date = self._to_local_date_from_datetime(raw_to)
|
||
else:
|
||
from_date = self._to_date(raw_from)
|
||
to_date = self._to_date(raw_to)
|
||
|
||
days = (to_date - from_date).days + 1
|
||
if days > 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="要求完成时间",
|
||
)
|
||
self._validate_range_days(
|
||
"created_date_from", "created_date_to", mode="date", label="创建日期"
|
||
)
|
||
return super().qs
|
||
|
||
|
||
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(CustomerVisibilityFilterMixin, 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: 排序字段
|
||
|
||
权限控制:
|
||
- 默认按客户可见性过滤(员工只能看到自己负责的客户的订单)
|
||
- 拥有 printing.view_all_plateorders 权限可突破此限制
|
||
"""
|
||
|
||
queryset = models.PlateOrder.objects.all()
|
||
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,
|
||
]
|
||
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()
|
||
|
||
# 默认不返回作废订单(仅 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
|
||
):
|
||
queryset = queryset.filter(is_invalid=False)
|
||
|
||
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)和商户(merchant),不允许前端传参控制。
|
||
"""
|
||
user = self.request.user
|
||
merchant = None
|
||
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"])
|
||
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})
|