1
0
forked from erp-dev/erp
Files
erpnew/api_v2/views/printing.py

889 lines
35 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.
import datetime
from django.utils import timezone
from django.db import transaction
from django.db.models import Q, Count, CharField, Prefetch
from django.db.models.functions import Cast, Coalesce
from rest_framework import serializers, status, permissions
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.response import Response
from rest_framework.views import APIView
from basic_info import models as basic_models
from printing import models as printing_models
from api_man.serializers import ProductSerializer
from stateflow import models as stateflow_models
class IsPrintingFactory(permissions.BasePermission):
"""仅允许印染工厂用户访问(与 api_v1 逻辑保持一致)"""
message = '您没有访问印染订单的权限'
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
if hasattr(request.user, 'employee'):
return request.user.employee.merchant.type == basic_models.MerchantTypeEnum.FACTORY
return False
class PrintingJobV2Serializer(serializers.ModelSerializer):
"""v2 独立的印染任务序列化器,包含开单数量"""
billed_quantity = serializers.DecimalField(max_digits=18, decimal_places=2, read_only=True)
business_object_id = serializers.SerializerMethodField()
width = serializers.SerializerMethodField()
fabric = serializers.SerializerMethodField()
product = ProductSerializer(read_only=True)
class Meta:
model = printing_models.PrintingJob
fields = [
'id',
'printing_order',
'product',
'work_state',
'quantity',
'width',
'fabric',
'unit',
'size',
'pieces',
'description',
'business_object_id',
'created_at',
'updated_at',
'billed_quantity',
]
read_only_fields = ['id', 'created_at', 'updated_at', 'billed_quantity']
def get_business_object_id(self, obj):
return obj.business_object_id
def get_width(self, obj: printing_models.PrintingJob) -> float:
return obj.printing_order.width
def get_fabric(self, obj: printing_models.PrintingJob) -> str:
return obj.printing_order.fabric
class PrintingJobByCustomerView(APIView):
"""
按客户与日期范围查询印染任务。
必填 query 参数:
- customer_id: 客户 ID
- date_from: 开始日期 (YYYY-MM-DD)
- date_to: 结束日期 (YYYY-MM-DD),闭区间,包含 23:59:59
可选过滤:
- printing_order: 按印染主订单 ID
- product_id / product_name / product_human_id / product_width_size / product_color
"""
serializer_class = PrintingJobV2Serializer
permission_classes = [permissions.AllowAny]
def get(self, request):
qp = request.query_params
customer_id = qp.get('customer_id')
date_from = qp.get('date_from')
date_to = qp.get('date_to')
printing_order_id = qp.get('printing_order')
product_id = qp.get('product_id')
product_name = qp.get('product_name')
product_human_id = qp.get('product_human_id')
product_width_size = qp.get('product_width_size')
product_color = qp.get('product_color')
if not customer_id:
return Response({'detail': 'customer_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
if not date_from or not date_to:
return Response({'detail': 'date_from 与 date_to 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
try:
customer_id_int = int(customer_id)
except (TypeError, ValueError):
return Response({'detail': 'customer_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
try:
start_date = datetime.datetime.strptime(date_from, '%Y-%m-%d').date()
end_date = datetime.datetime.strptime(date_to, '%Y-%m-%d').date()
except ValueError:
return Response({'detail': '日期格式需为 YYYY-MM-DD'}, status=status.HTTP_400_BAD_REQUEST)
# 闭区间:包含当日 00:00:00 和 23:59:59.999999
start_dt = datetime.datetime.combine(start_date, datetime.time.min)
end_dt = datetime.datetime.combine(end_date, datetime.time.max)
if timezone.is_naive(start_dt):
start_dt = timezone.make_aware(start_dt, timezone.get_default_timezone())
if timezone.is_naive(end_dt):
end_dt = timezone.make_aware(end_dt, timezone.get_default_timezone())
queryset = printing_models.PrintingJob.objects.select_related('printing_order', 'product').filter(
printing_order__customer_id=customer_id_int,
created_at__gte=start_dt,
created_at__lte=end_dt,
)
# 可选过滤printing_order
if printing_order_id:
try:
queryset = queryset.filter(printing_order_id=int(printing_order_id))
except (TypeError, ValueError):
return Response({'detail': 'printing_order 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
# 可选过滤product
if product_id:
try:
queryset = queryset.filter(product_id=int(product_id))
except (TypeError, ValueError):
return Response({'detail': 'product_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
if product_name:
queryset = queryset.filter(product__name__icontains=product_name)
if product_human_id:
queryset = queryset.filter(product__human_id__icontains=product_human_id)
if product_width_size:
try:
width_decimal = float(product_width_size)
except (TypeError, ValueError):
return Response({'detail': 'product_width_size 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
queryset = queryset.filter(product__width_size=width_decimal)
if product_color:
queryset = queryset.filter(product__color__icontains=product_color)
queryset = queryset.order_by('-created_at')
serializer = self.serializer_class(queryset, many=True)
return Response(serializer.data)
class PrintingJobBatchAdvancePreviewRequestSerializer(serializers.Serializer):
"""批量推进:预览/校验请求"""
printing_job_ids = serializers.ListField(
child=serializers.IntegerField(min_value=1),
allow_empty=False,
help_text='需要批量推进的 printing_job id 列表',
)
def validate_printing_job_ids(self, value):
# 去重保持稳定性(前端可能重复传)
deduped = list(dict.fromkeys(value))
if not deduped:
raise serializers.ValidationError('printing_job_ids 不能为空')
return deduped
class PrintingJobBatchAdvanceSubmitRequestSerializer(PrintingJobBatchAdvancePreviewRequestSerializer):
"""批量推进:提交请求"""
parameters = serializers.DictField(
child=serializers.JSONField(),
required=False,
default=dict,
help_text='与单条推进接口一致的工艺参数(将作为 **kwargs 传给 stateflow',
)
def _validate_jobs_for_batch_advance(printing_job_ids: list[int]):
"""
批量推进的核心一致性校验preview 与 submit 共用)
规则:
1) 所有 id 都存在
2) 全部属于同一个 printing_order
3) 全部存在 business_object流程实例
4) 全部具有相同的 next_pending_state下一待执行节点否则不允许批量
注意这里不做“竞态”处理preview 后 submit 前状态变化submit 时会再次调用该函数重新校验。
后续如需增强,可在 preview 返回 snapshot token在 submit 校验 token 以提升用户体验。
"""
# 查询并校验存在性
qs = (
printing_models.PrintingJob.objects
.select_related('printing_order', 'business_object', 'business_object__process')
.filter(id__in=printing_job_ids)
)
jobs = list(qs)
found_ids = {j.id for j in jobs}
missing_ids = [str(i) for i in printing_job_ids if i not in found_ids]
if missing_ids:
raise serializers.ValidationError({'detail': f'以下 printing_job 不存在: {", ".join(missing_ids)}'})
# 同一订单
order_ids = {j.printing_order_id for j in jobs}
if len(order_ids) != 1:
raise serializers.ValidationError({'detail': '所选明细不属于同一个 printing_order无法批量推进'})
printing_order_id = next(iter(order_ids))
printing_order = jobs[0].printing_order
# 必须有关联流程实例
no_bo = [str(j.id) for j in jobs if not j.business_object_id]
if no_bo:
raise serializers.ValidationError({'detail': f'以下 printing_job 未关联流程实例business_object无法推进: {", ".join(no_bo)}'})
# 计算并校验 next_pending_state 一致
from stateflow import services as stateflow_services
next_infos = []
for j in jobs:
info = stateflow_services.get_next_pending_state(j.business_object, include_parameters=True)
if info is None:
next_infos.append((j.id, None))
else:
next_infos.append((j.id, info))
# 不能包含“无待执行节点”(流程已完成或无节点)
cannot_advance = [str(job_id) for job_id, info in next_infos if info is None]
if cannot_advance:
raise serializers.ValidationError({'detail': f'以下 printing_job 没有待执行节点(流程已完成或无节点),无法批量推进: {", ".join(cannot_advance)}'})
# 比对 state_id
first_info = next_infos[0][1]
target_state = first_info['state']
target_order = first_info['order']
target_state_id = target_state.id
diff_jobs = []
for job_id, info in next_infos:
if info['state'].id != target_state_id:
diff_jobs.append(str(job_id))
if diff_jobs:
raise serializers.ValidationError({'detail': f'所选明细当前待执行节点不一致,无法批量推进(不同节点的 jobs: {", ".join(diff_jobs)}'})
# 参数定义取目标节点(所有一致)
target_parameters = first_info.get('parameters', []) or []
return {
'printing_order': printing_order,
'printing_order_id': printing_order_id,
'jobs': jobs,
'target_state': target_state,
'target_order': target_order,
'target_parameters': target_parameters,
}
class PrintingJobBatchAdvancePreviewView(APIView):
"""
批量推进:预览
作用:
- 校验 printing_job_ids 是否可批量推进(同订单/同待执行节点)
- 返回“下一步待执行节点”的信息及其工艺参数定义,供前端生成批量表单
"""
permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
def post(self, request):
srz = PrintingJobBatchAdvancePreviewRequestSerializer(data=request.data)
srz.is_valid(raise_exception=True)
data = _validate_jobs_for_batch_advance(srz.validated_data['printing_job_ids'])
from stateflow.serializers import StateParameterSerializer
params_srz = StateParameterSerializer(
data['target_parameters'],
many=True,
context={'request': request},
)
return Response({
'printing_order_id': data['printing_order_id'],
'printing_job_ids': [j.id for j in data['jobs']],
'next_state': {
'id': data['target_state'].id,
'name': data['target_state'].name,
'description': data['target_state'].description,
'order': data['target_order'],
'parameters': params_srz.data,
}
})
class PrintingJobBatchAdvanceSubmitView(APIView):
"""
批量推进:提交
规则:全成功/全失败
- 任意一个 job 推进失败:整体回滚(不产生任何 stateflow 日志,也不产生批量推进记录)
"""
permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
def post(self, request):
srz = PrintingJobBatchAdvanceSubmitRequestSerializer(data=request.data)
srz.is_valid(raise_exception=True)
payload = srz.validated_data
parameters = payload.get('parameters') or {}
data = _validate_jobs_for_batch_advance(payload['printing_job_ids'])
from stateflow import services as stateflow_services
# 全成功/全失败:用事务包住整个批量推进
with transaction.atomic():
record = printing_models.PrintingJobBatchAdvanceRecord.objects.create(
printing_order=data['printing_order'],
state=data['target_state'],
created_by=request.user,
parameters=parameters,
)
record.printing_jobs.set(data['jobs'])
# 逐个复用单条推进逻辑
last_message = None
for job in data['jobs']:
# 修复历史/异常数据:确保该 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}",
)
ok, msg, _state_log = stateflow_services.advance_to_next_state(
job.business_object,
request.user,
**parameters
)
if not ok:
# 抛异常触发事务回滚,保证“全部失败”
raise serializers.ValidationError({'detail': msg})
last_message = msg
# 返回最新的 job 列表(可用于前端刷新)
refreshed_jobs = (
printing_models.PrintingJob.objects
.select_related('printing_order', 'product')
.filter(id__in=[j.id for j in data['jobs']])
.order_by('id')
)
job_srz = PrintingJobV2Serializer(refreshed_jobs, many=True)
return Response({
'detail': last_message or '批量推进成功',
'batch_id': record.id,
'printing_order_id': data['printing_order_id'],
'printing_job_ids': [j.id for j in data['jobs']],
'jobs': job_srz.data,
})
class PlateOrderByProcessNodeSerializer(serializers.ModelSerializer):
"""按流程节点筛选 PlateOrder 的列表序列化(尽量保持轻量)"""
design_code = serializers.SerializerMethodField()
customer_name = serializers.CharField(source='customer.name', read_only=True)
business_object_id = serializers.SerializerMethodField()
created_by = serializers.IntegerField(source='created_by_id', read_only=True)
process_parameters = serializers.SerializerMethodField()
class Meta:
model = printing_models.PlateOrder
fields = [
'id',
'design_code',
'customer',
'customer_name',
'style_name',
'urgency_level',
'is_invalid',
'business_object_id',
'created_by',
'process_parameters',
'created_at',
'updated_at',
]
read_only_fields = fields
def get_design_code(self, obj: printing_models.PlateOrder) -> str | None:
return obj.design_code or (str(obj.id) if obj.id else None)
def get_business_object_id(self, obj: printing_models.PlateOrder) -> int | None:
return obj.business_object_id
def get_process_parameters(self, obj: printing_models.PlateOrder) -> list[dict]:
"""
返回“订单维度”的工艺参数 key/value当前 process_node 对应 state 的参数)。
取值来源:
- 优先取该订单 business_object 在目标 state 的**最新一次 StateFlowRecord**(可能是已撤销记录)
对应的 StateLogParameterRecord 汇总(后提交覆盖先提交)。
- 若从未提交过该 state 的参数value 为 null由前端自行用顶层 parameters 的默认值做兜底/占位)
"""
keys: list[str] = self.context.get('target_parameter_keys') or []
target_state_id: int | None = self.context.get('target_state_id')
if not keys or not target_state_id:
return [{'key': k, 'value': None} for k in keys]
bo = getattr(obj, 'business_object', None)
if not bo:
return [{'key': k, 'value': None} for k in keys]
logs = getattr(bo, '_prefetched_target_state_logs', None)
if logs is None:
# fallback极少数情况下未预取
logs = list(
bo.state_logs.filter(state_id=target_state_id).order_by('-completed_at', '-id')[:1]
)
latest_log = logs[0] if logs else None
summary: dict = {}
if latest_log:
param_records = getattr(latest_log, '_prefetched_parameter_records', None)
if param_records is None:
param_records = list(latest_log.parameter_records.all().order_by('created_at', 'id'))
for rec in param_records:
summary.update(rec.parameters or {})
return [{'key': k, 'value': summary.get(k)} for k in keys]
class PlateOrderByProcessNodeView(APIView):
"""
按 process_node_id 查询“当前处于该节点NEXT 模式:下一个待执行节点)”的 PlateOrder 列表。
GET /api/v2/plate-orders/by-process-node/
Query 参数:
- process_node_id: 必填ProcessNode.id
- search: 可选。支持:
- 纯数字:同时匹配 id 精确 + design_code icontains
- 非纯数字design_code icontains
- ordering: 可选,默认 -created_at支持: id / created_at / updated_at / design_code
- limit/offset: 分页limit 默认 20
"""
permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
_ORDERING_FIELDS = {'id', 'created_at', 'updated_at', 'design_code'}
def get(self, request):
qp = request.query_params
process_node_id = qp.get('process_node_id')
if not process_node_id:
return Response({'detail': 'process_node_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
try:
process_node_id_int = int(process_node_id)
except (TypeError, ValueError):
return Response({'detail': 'process_node_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
try:
process_node = (
stateflow_models.ProcessNode.objects
.select_related('process', 'state')
.get(id=process_node_id_int)
)
except stateflow_models.ProcessNode.DoesNotExist:
return Response({'detail': 'process_node 不存在'}, status=status.HTTP_404_NOT_FOUND)
# 目标节点信息
target_state_id = process_node.state_id
target_order = process_node.order
process_id = process_node.process_id
# 目标节点之前的所有 state_iddistinct便于计数
prev_state_ids = list(
stateflow_models.ProcessNode.objects
.filter(process_id=process_id, order__lt=target_order)
.order_by('order', 'id')
.values_list('state_id', flat=True)
.distinct()
)
queryset = (
printing_models.PlateOrder.objects
.select_related('customer', 'business_object')
.filter(business_object__isnull=False, business_object__process_id=process_id)
)
# NEXT 模式判定:前置节点都已完成(未撤销) + 目标节点尚未完成(未撤销)
if prev_state_ids:
queryset = queryset.annotate(
completed_prev_states=Count(
'business_object__state_logs__state_id',
filter=Q(
business_object__state_logs__is_cancelled=False,
business_object__state_logs__state_id__in=prev_state_ids,
),
distinct=True,
)
).filter(completed_prev_states=len(prev_state_ids))
queryset = queryset.annotate(
completed_target=Count(
'business_object__state_logs__id',
filter=Q(
business_object__state_logs__is_cancelled=False,
business_object__state_logs__state_id=target_state_id,
),
distinct=True,
)
).filter(completed_target=0)
# search同时支持主键与 design_code icontains不新增额外参数
search = (qp.get('search') or '').strip()
if search:
if search.isdigit():
try:
search_id = int(search)
except (TypeError, ValueError):
search_id = None
cond = Q(design_code__icontains=search)
if search_id is not None:
cond = cond | Q(id=search_id)
queryset = queryset.filter(cond)
else:
queryset = queryset.filter(design_code__icontains=search)
# ordering默认 -created_at
ordering = (qp.get('ordering') or '-created_at').strip() or '-created_at'
direction = '-' if ordering.startswith('-') else ''
field = ordering[1:] if ordering.startswith('-') else ordering
if field not in self._ORDERING_FIELDS:
return Response(
{'detail': f'ordering 不支持: {ordering}(可选: {", ".join(sorted(self._ORDERING_FIELDS))}'},
status=status.HTTP_400_BAD_REQUEST,
)
# 为 design_code 排序提供兜底:为空时按主键字符串
if field == 'design_code':
queryset = queryset.annotate(
design_code_normalized=Coalesce('design_code', Cast('id', output_field=CharField()))
).order_by(f'{direction}design_code_normalized', 'id')
else:
queryset = queryset.order_by(f'{direction}{field}', 'id')
# 该节点参数模板(仅 key/value同时用于 results[*].process_parameters 的 key 集合
params = list(process_node.state.parameters.order_by('id').values('key', 'value'))
target_keys = [p['key'] for p in params]
# 预取:目标 state 的最新日志及其参数记录(用于拼装“订单维度”的参数值,避免 N+1
param_records_prefetch = Prefetch(
'parameter_records',
queryset=stateflow_models.StateLogParameterRecord.objects.order_by('created_at', 'id'),
to_attr='_prefetched_parameter_records',
)
target_state_logs_qs = (
stateflow_models.StateFlowRecord.objects
.filter(state_id=target_state_id)
.order_by('-completed_at', '-id')
.prefetch_related(param_records_prefetch)
)
queryset = queryset.prefetch_related(
Prefetch(
'business_object__state_logs',
queryset=target_state_logs_qs,
to_attr='_prefetched_target_state_logs',
)
)
# 分页limit 默认 20
paginator = LimitOffsetPagination()
paginator.default_limit = 20
page = paginator.paginate_queryset(queryset, request, view=self)
results = page if page is not None else list(queryset)
srz = PlateOrderByProcessNodeSerializer(
results,
many=True,
context={
'request': request,
'target_state_id': target_state_id,
'target_parameter_keys': target_keys,
},
)
return Response({
'process_node': {
'id': process_node.id,
'process_id': process_id,
'state_id': target_state_id,
'state_name': process_node.state.name,
'order': target_order,
},
'parameters': params,
'count': getattr(paginator, 'count', len(results)),
'next': paginator.get_next_link() if page is not None else None,
'previous': paginator.get_previous_link() if page is not None else None,
'results': srz.data,
})
class PlateOrderByProcessSerializer(serializers.ModelSerializer):
"""按流程process_id查询 PlateOrder附带所有节点的参数汇总订单维度"""
design_code = serializers.SerializerMethodField()
customer_name = serializers.CharField(source='customer.name', read_only=True)
business_object_id = serializers.SerializerMethodField()
created_by = serializers.IntegerField(source='created_by_id', read_only=True)
process_params = serializers.SerializerMethodField()
class Meta:
model = printing_models.PlateOrder
fields = [
'id',
'design_code',
'customer',
'customer_name',
'style_name',
'urgency_level',
'is_invalid',
'business_object_id',
'created_by',
'process_params',
'created_at',
'updated_at',
]
read_only_fields = fields
def get_design_code(self, obj: printing_models.PlateOrder) -> str | None:
return obj.design_code or (str(obj.id) if obj.id else None)
def get_business_object_id(self, obj: printing_models.PlateOrder) -> int | None:
return obj.business_object_id
def get_process_params(self, obj: printing_models.PlateOrder) -> list[dict]:
"""
返回所有流程节点的参数视图(订单维度)。
约束(前端可依赖):
- 一定包含 process 的全部节点(按 order 升序)
- 每个节点包含 is_executed默认不含撤销记录
- params 的 key/顺序与该节点 State.parameters 的 key/顺序一致
- 未执行或未提交的参数 value 为 null
"""
nodes_info: list[dict] = self.context.get('process_nodes_info') or []
if not nodes_info:
return []
bo = getattr(obj, 'business_object', None)
if not bo:
# 没有关联流程实例:全部视为未执行
result = []
for node in nodes_info:
result.append({
'process_node_id': node['process_node_id'],
'state_id': node['state_id'],
'node_name': node['node_name'],
'order': node['order'],
'is_executed': False,
'params': [{'key': k, 'value': None} for k in (node.get('keys') or [])],
})
return result
logs = getattr(bo, '_prefetched_state_logs_for_process_params', None)
if logs is None:
state_ids = [n['state_id'] for n in nodes_info]
logs = list(
bo.state_logs.filter(is_cancelled=False, state_id__in=state_ids)
.order_by('-completed_at', '-id')
.prefetch_related('parameter_records')
)
# logs 已按时间倒序:第一次出现的 state_id 即“最新一次非撤销执行记录”
latest_log_by_state: dict[int, stateflow_models.StateFlowRecord] = {}
for log in logs:
if log.state_id not in latest_log_by_state:
latest_log_by_state[log.state_id] = log
result = []
for node in nodes_info:
state_id = node['state_id']
keys = node.get('keys') or []
log = latest_log_by_state.get(state_id)
is_executed = log is not None
summary: dict = {}
if log is not None:
param_records = getattr(log, '_prefetched_parameter_records', None)
if param_records is None:
param_records = list(log.parameter_records.all().order_by('created_at', 'id'))
for rec in param_records:
summary.update(rec.parameters or {})
result.append({
'process_node_id': node['process_node_id'],
'state_id': state_id,
'node_name': node['node_name'],
'order': node['order'],
'is_executed': is_executed,
'params': [{'key': k, 'value': summary.get(k) if is_executed else None} for k in keys],
})
return result
class PlateOrderByProcessView(APIView):
"""
按 process_id 查询 PlateOrder 列表,并在每条 PlateOrder 中返回 process 的所有节点参数结构。
GET /api/v2/plate-orders/by-process/
Query 参数:
- process_id: 必填Process.id
- date_from/date_to: 必填YYYY-MM-DD按 PlateOrder.created_at 闭区间过滤)
- plate_order: 可选。支持:
- 纯数字:同时匹配 id 精确 + design_code icontains
- 非纯数字design_code icontains
- ordering: 可选,默认 -created_at支持: id / created_at / updated_at / design_code
- limit/offset: 分页limit 默认 20
"""
permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
_ORDERING_FIELDS = {'id', 'created_at', 'updated_at', 'design_code'}
def get(self, request):
qp = request.query_params
process_id = qp.get('process_id')
if not process_id:
return Response({'detail': 'process_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
try:
process_id_int = int(process_id)
except (TypeError, ValueError):
return Response({'detail': 'process_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
date_from = qp.get('date_from')
date_to = qp.get('date_to')
if not date_from or not date_to:
return Response({'detail': 'date_from 与 date_to 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
try:
start_date = datetime.datetime.strptime(date_from, '%Y-%m-%d').date()
end_date = datetime.datetime.strptime(date_to, '%Y-%m-%d').date()
except ValueError:
return Response({'detail': '日期格式需为 YYYY-MM-DD'}, status=status.HTTP_400_BAD_REQUEST)
# 闭区间:包含当日 00:00:00 和 23:59:59.999999
start_dt = datetime.datetime.combine(start_date, datetime.time.min)
end_dt = datetime.datetime.combine(end_date, datetime.time.max)
if timezone.is_naive(start_dt):
start_dt = timezone.make_aware(start_dt, timezone.get_default_timezone())
if timezone.is_naive(end_dt):
end_dt = timezone.make_aware(end_dt, timezone.get_default_timezone())
try:
process = stateflow_models.Process.objects.get(id=process_id_int)
except stateflow_models.Process.DoesNotExist:
return Response({'detail': 'process 不存在'}, status=status.HTTP_404_NOT_FOUND)
# 该 process 的所有节点(含参数定义 key 顺序)
params_prefetch = Prefetch(
'state__parameters',
queryset=stateflow_models.StateParameter.objects.order_by('id'),
)
process_nodes = list(
stateflow_models.ProcessNode.objects
.filter(process_id=process_id_int)
.select_related('state')
.prefetch_related(params_prefetch)
.order_by('order', 'id')
)
process_nodes_info = []
state_ids = []
for pn in process_nodes:
keys = [p.key for p in pn.state.parameters.all()]
process_nodes_info.append({
'process_node_id': pn.id,
'state_id': pn.state_id,
'node_name': pn.state.name,
'order': pn.order,
'keys': keys,
})
state_ids.append(pn.state_id)
queryset = (
printing_models.PlateOrder.objects
.select_related('customer', 'business_object')
.filter(
process=process_id_int,
created_at__gte=start_dt,
created_at__lte=end_dt,
)
)
# plate_order同时支持主键与 design_code icontains不新增额外参数
plate_order = (qp.get('plate_order') or '').strip()
if plate_order:
if plate_order.isdigit():
try:
pid = int(plate_order)
except (TypeError, ValueError):
pid = None
cond = Q(design_code__icontains=plate_order)
if pid is not None:
cond = cond | Q(id=pid)
queryset = queryset.filter(cond)
else:
queryset = queryset.filter(design_code__icontains=plate_order)
# ordering默认 -created_at
ordering = (qp.get('ordering') or '-created_at').strip() or '-created_at'
direction = '-' if ordering.startswith('-') else ''
field = ordering[1:] if ordering.startswith('-') else ordering
if field not in self._ORDERING_FIELDS:
return Response(
{'detail': f'ordering 不支持: {ordering}(可选: {", ".join(sorted(self._ORDERING_FIELDS))}'},
status=status.HTTP_400_BAD_REQUEST,
)
if field == 'design_code':
queryset = queryset.annotate(
design_code_normalized=Coalesce('design_code', Cast('id', output_field=CharField()))
).order_by(f'{direction}design_code_normalized', 'id')
else:
queryset = queryset.order_by(f'{direction}{field}', 'id')
# 预取:非撤销的状态日志及其参数记录(用于计算 is_executed + value
state_ids_unique = sorted(set(state_ids))
param_records_prefetch = Prefetch(
'parameter_records',
queryset=stateflow_models.StateLogParameterRecord.objects.order_by('created_at', 'id'),
to_attr='_prefetched_parameter_records',
)
logs_qs = (
stateflow_models.StateFlowRecord.objects
.filter(is_cancelled=False)
.order_by('-completed_at', '-id')
.prefetch_related(param_records_prefetch)
)
if state_ids_unique:
logs_qs = logs_qs.filter(state_id__in=state_ids_unique)
queryset = queryset.prefetch_related(
Prefetch(
'business_object__state_logs',
queryset=logs_qs,
to_attr='_prefetched_state_logs_for_process_params',
)
)
paginator = LimitOffsetPagination()
paginator.default_limit = 20
page = paginator.paginate_queryset(queryset, request, view=self)
results = page if page is not None else list(queryset)
srz = PlateOrderByProcessSerializer(
results,
many=True,
context={
'request': request,
'process_nodes_info': process_nodes_info,
},
)
return Response({
'process': {
'id': process.id,
'name': process.name,
'node_count': len(process_nodes_info),
},
'count': getattr(paginator, 'count', len(results)),
'next': paginator.get_next_link() if page is not None else None,
'previous': paginator.get_previous_link() if page is not None else None,
'results': srz.data,
})