forked from erp-dev/erp
1193 lines
46 KiB
Python
1193 lines
46 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')
|
||
|
||
class Meta:
|
||
model = models.PrintingOrder
|
||
fields = ['customer', 'is_urgent', 'is_fabric_received', 'is_invalid', 'external_order_id']
|
||
|
||
@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
|
||
})
|