1
0
forked from erp-dev/erp

feat: new api for plate order (get_plate_order_by_state_status)

This commit is contained in:
2025-12-25 17:18:44 +08:00
parent 2c5174a1ec
commit ff1ea2482d
21 changed files with 936 additions and 58 deletions

View File

@@ -10,6 +10,7 @@ from .printing import (
PrintingJobBatchAdvanceSubmitView,
PlateOrderByProcessNodeView,
PlateOrderByProcessView,
PlateOrderByStateStatusView,
PlateOrderBatchUpdateView,
)
from .stateflow import BusinessObjectCloneView
@@ -22,6 +23,7 @@ __all__ = [
'PrintingJobBatchAdvanceSubmitView',
'PlateOrderByProcessNodeView',
'PlateOrderByProcessView',
'PlateOrderByStateStatusView',
'PlateOrderBatchUpdateView',
'BusinessObjectCloneView',
]

View File

@@ -3,7 +3,7 @@ import datetime
from django.utils import timezone
from django.db import transaction
from django.db import models as django_models
from django.db.models import Q, Count, CharField, Prefetch
from django.db.models import Q, Count, CharField, Prefetch, Exists, OuterRef
from django.db.models.functions import Cast, Coalesce
from rest_framework import serializers, status, permissions
from rest_framework.pagination import LimitOffsetPagination
@@ -14,6 +14,7 @@ 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
from stateflow import services as stateflow_services
class IsPrintingFactory(permissions.BasePermission):
@@ -721,6 +722,102 @@ class PlateOrderByProcessSerializer(serializers.ModelSerializer):
return result
class PlateOrderByStateStatusSerializer(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)
state_parameters = serializers.SerializerMethodField()
state_log = serializers.SerializerMethodField()
state_status = 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',
'state_status',
'state_parameters',
'state_log',
'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_state_status(self, obj: printing_models.PlateOrder) -> str:
return self.context.get('requested_status', '')
def _get_latest_state_log(self, obj: printing_models.PlateOrder):
bo = getattr(obj, 'business_object', None)
if not bo:
return None
logs = getattr(bo, '_prefetched_target_state_logs', None)
if logs:
return logs[0]
target_state_id = self.context.get('target_state_id')
if not target_state_id:
return None
return (
bo.state_logs
.filter(state_id=target_state_id)
.order_by('-completed_at', '-id')
.select_related('completed_by')
.first()
)
def get_state_parameters(self, obj: printing_models.PlateOrder) -> list[dict]:
keys: list[str] = self.context.get('target_parameter_keys') or []
if not keys:
return []
bo = getattr(obj, 'business_object', None)
if not bo:
return [{'key': k, 'value': None} for k in keys]
latest_log = self._get_latest_state_log(obj)
if latest_log is None:
return [{'key': k, 'value': None} for k in keys]
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'))
summary: dict = {}
for rec in param_records:
summary.update(rec.parameters or {})
return [{'key': k, 'value': summary.get(k)} for k in keys]
def get_state_log(self, obj: printing_models.PlateOrder) -> dict | None:
log = self._get_latest_state_log(obj)
if log is None:
return None
completed_by = log.completed_by
return {
'id': log.id,
'state_id': log.state_id,
'completed_at': log.completed_at,
'completed_by': completed_by.id if completed_by else None,
'completed_by_username': completed_by.username if completed_by else None,
'is_cancelled': log.is_cancelled,
}
class PlateOrderByProcessView(APIView):
"""
按 process_id 查询 PlateOrder 列表,并在每条 PlateOrder 中返回 process 的所有节点参数结构。
@@ -892,6 +989,176 @@ class PlateOrderByProcessView(APIView):
})
class PlateOrderByStateStatusView(APIView):
"""按 process_id + state_id + status 过滤 PlateOrder。"""
permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
_ORDERING_FIELDS = {'id', 'created_at', 'updated_at', 'design_code'}
_SUPPORTED_STATUS = {'completed', 'not_started', 'cancelled', 'in_progress'}
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)
state_id_raw = qp.get('state_id')
state_id_int: int | None = None
if state_id_raw not in (None, ''):
try:
state_id_int = int(state_id_raw)
except (TypeError, ValueError):
return Response({'detail': 'state_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
status_value = (qp.get('status') or 'completed').strip().lower()
if state_id_int is None:
if qp.get('status') and status_value != 'not_started':
return Response({'detail': 'state_id 为空时仅支持 status=not_started'}, status=status.HTTP_400_BAD_REQUEST)
status_value = 'not_started'
elif status_value == 'not_started':
return Response({'detail': 'state_id 不为空时不支持 status=not_started'}, status=status.HTTP_400_BAD_REQUEST)
elif status_value not in self._SUPPORTED_STATUS:
options = ', '.join(sorted(self._SUPPORTED_STATUS))
return Response({'detail': f'status 不支持: {status_value}(可选: {options}'}, status=status.HTTP_400_BAD_REQUEST)
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)
state = None
state_parameters_template: list[dict] = []
target_keys: list[str] = []
representative_order: int | None = None
process_node_ids: list[int] = []
base_bo = stateflow_models.BusinessObject.objects.filter(
process_id=process_id_int,
plate_order__isnull=False,
)
if state_id_int is None:
filtered_bo = base_bo.annotate(
has_any_log=Exists(
stateflow_models.StateFlowRecord.objects.filter(business_object_id=OuterRef('pk'))
)
).filter(has_any_log=False)
queryset = (
printing_models.PlateOrder.objects
.select_related('customer', 'business_object', 'created_by')
.filter(process=process_id_int, business_object_id__in=filtered_bo.values('id'))
)
else:
try:
state = stateflow_models.State.objects.get(id=state_id_int)
except stateflow_models.State.DoesNotExist:
return Response({'detail': 'state 不存在'}, status=status.HTTP_404_NOT_FOUND)
process_nodes = list(
stateflow_models.ProcessNode.objects
.filter(process_id=process_id_int, state_id=state_id_int)
.order_by('order', 'id')
)
if not process_nodes:
return Response({'detail': 'state 不属于该 process'}, status=status.HTTP_400_BAD_REQUEST)
state_parameters_template = [
{'key': param.key, 'value': param.value}
for param in state.parameters.order_by('id')
]
target_keys = [item['key'] for item in state_parameters_template]
representative_order = process_nodes[0].order
process_node_ids = [node.id for node in process_nodes]
filtered_bo = stateflow_services.query_business_objects_by_state_status(
state_ids=[state_id_int],
status=status_value,
process_id=process_id_int,
base_queryset=base_bo,
)
queryset = (
printing_models.PlateOrder.objects
.select_related('customer', 'business_object', 'created_by')
.filter(process=process_id_int, business_object_id__in=filtered_bo.values('id'))
)
param_records_prefetch = Prefetch(
'parameter_records',
queryset=stateflow_models.StateLogParameterRecord.objects.order_by('created_at', 'id'),
to_attr='_prefetched_parameter_records',
)
target_logs_qs = (
stateflow_models.StateFlowRecord.objects
.filter(state_id=state_id_int)
.order_by('-completed_at', '-id')
.select_related('completed_by')
.prefetch_related(param_records_prefetch)
)
queryset = queryset.prefetch_related(
Prefetch(
'business_object__state_logs',
queryset=target_logs_qs,
to_attr='_prefetched_target_state_logs',
)
)
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:
options = ', '.join(sorted(self._ORDERING_FIELDS))
return Response({'detail': f'ordering 不支持: {ordering}(可选: {options}'}, 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')
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 = PlateOrderByStateStatusSerializer(
results,
many=True,
context={
'request': request,
'target_state_id': state_id_int,
'target_parameter_keys': target_keys,
'requested_status': status_value,
},
)
return Response({
'process': {
'id': process.id,
'name': process.name,
},
'state': {
'id': state.id if state else None,
'name': state.name if state else None,
'order': representative_order,
'process_node_ids': process_node_ids,
} if state else None,
'status': status_value,
'state_parameters': state_parameters_template,
'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,
})
# 允许批量更新的字段白名单:只改这里即可增减
PLATE_ORDER_BATCH_UPDATE_ALLOWED_FIELDS = [
"original_id",