1
0
forked from erp-dev/erp

feat: added process_node parameters view for plate order

This commit is contained in:
2025-12-16 16:15:13 +08:00
parent 0638dd1ad3
commit 639efe8421
7 changed files with 559 additions and 0 deletions

View File

@@ -8,6 +8,7 @@ from .printing import (
PrintingJobV2Serializer,
PrintingJobBatchAdvancePreviewView,
PrintingJobBatchAdvanceSubmitView,
PlateOrderByProcessNodeView,
)
from .stateflow import BusinessObjectCloneView
@@ -17,6 +18,7 @@ __all__ = [
'PrintingJobV2Serializer',
'PrintingJobBatchAdvancePreviewView',
'PrintingJobBatchAdvanceSubmitView',
'PlateOrderByProcessNodeView',
'BusinessObjectCloneView',
]

View File

@@ -2,13 +2,17 @@ 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):
@@ -360,3 +364,243 @@ class PrintingJobBatchAdvanceSubmitView(APIView):
'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()
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',
'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,
})