forked from erp-dev/erp
fix: business.object_id maybe null, missing parameters when plate-Order cloned
This commit is contained in:
@@ -338,6 +338,12 @@ class PrintingJobBatchAdvanceSubmitView(APIView):
|
||||
# 逐个复用单条推进逻辑
|
||||
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,
|
||||
@@ -372,6 +378,7 @@ class PlateOrderByProcessNodeSerializer(serializers.ModelSerializer):
|
||||
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:
|
||||
@@ -385,6 +392,7 @@ class PlateOrderByProcessNodeSerializer(serializers.ModelSerializer):
|
||||
'urgency_level',
|
||||
'is_invalid',
|
||||
'business_object_id',
|
||||
'created_by',
|
||||
'process_parameters',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
@@ -604,3 +612,277 @@ class PlateOrderByProcessNodeView(APIView):
|
||||
'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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user