forked from erp-dev/erp
1790 lines
69 KiB
Python
1790 lines
69 KiB
Python
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, Exists, OuterRef, Subquery
|
||
from django.db.models.fields.json import KeyTextTransform
|
||
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_v1.tasks import (
|
||
ExternalPrintingOrderSnapshotSyncError,
|
||
sync_external_printing_order_snapshot_impl,
|
||
)
|
||
from api_man.serializers import ProductSerializer
|
||
from api_v1.views.printing.serializers import (
|
||
PlateOrderListSerializer as PlateOrderListV1Serializer,
|
||
_serialize_plate_images,
|
||
)
|
||
from stateflow import models as stateflow_models
|
||
from stateflow import services as stateflow_services
|
||
|
||
|
||
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
|
||
|
||
|
||
def _serialize_plate_image_first(raw_value, request):
|
||
images = _serialize_plate_images(raw_value, request)
|
||
if not images:
|
||
return []
|
||
return images[:1]
|
||
|
||
|
||
class PrintingJobV2Serializer(serializers.ModelSerializer):
|
||
"""v2 独立的印染任务序列化器,包含开单数量"""
|
||
|
||
external_order_id = serializers.CharField(source='printing_order.external_order_id', read_only=True)
|
||
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',
|
||
'original_id',
|
||
'printing_order',
|
||
'external_order_id',
|
||
'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', 'external_order_id', '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()
|
||
plate_image = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = printing_models.PlateOrder
|
||
fields = [
|
||
'id',
|
||
'original_id',
|
||
'design_code',
|
||
'plate_image',
|
||
'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]
|
||
|
||
def get_plate_image(self, obj: printing_models.PlateOrder) -> list[dict]:
|
||
return _serialize_plate_image_first(getattr(obj, 'plate_image', None), self.context.get('request'))
|
||
|
||
|
||
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')
|
||
param_key = (qp.get('param_key') or '').strip()
|
||
param_value = (qp.get('param_value') or '').strip()
|
||
date_from = qp.get('date_from')
|
||
date_to = qp.get('date_to')
|
||
if not process_node_id:
|
||
return Response({'detail': 'process_node_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
|
||
if (param_key and not param_value) or (param_value and not param_key):
|
||
return Response({'detail': 'param_key 与 param_value 必须同时提供'}, status=status.HTTP_400_BAD_REQUEST)
|
||
if (date_from and not date_to) or (date_to and not date_from):
|
||
return Response({'detail': 'date_from 与 date_to 必须同时提供'}, 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)
|
||
|
||
if date_from and date_to:
|
||
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())
|
||
|
||
# 目标节点信息
|
||
target_state_id = process_node.state_id
|
||
target_order = process_node.order
|
||
process_id = process_node.process_id
|
||
|
||
# 目标节点之前的所有 state_id(distinct,便于计数)
|
||
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)
|
||
)
|
||
|
||
if date_from and date_to:
|
||
queryset = queryset.filter(created_at__gte=start_dt, created_at__lte=end_dt)
|
||
|
||
# COMPLETED 模式判定:目标节点已完成(未撤销)
|
||
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__gt=0)
|
||
|
||
if param_key and param_value:
|
||
matching_logs = stateflow_models.StateFlowRecord.objects.filter(
|
||
business_object_id=OuterRef('business_object_id'),
|
||
state_id=target_state_id,
|
||
is_cancelled=False,
|
||
parameter_records__parameters__has_key=param_key,
|
||
).annotate(
|
||
_param_value=KeyTextTransform(param_key, 'parameter_records__parameters')
|
||
).filter(
|
||
_param_value__icontains=param_value,
|
||
)
|
||
queryset = queryset.annotate(_has_state_param=Exists(matching_logs)).filter(_has_state_param=True)
|
||
|
||
# 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()
|
||
plate_image = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = printing_models.PlateOrder
|
||
fields = [
|
||
'id',
|
||
'original_id',
|
||
'design_code',
|
||
'plate_image',
|
||
'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
|
||
|
||
def get_plate_image(self, obj: printing_models.PlateOrder) -> list[dict]:
|
||
return _serialize_plate_image_first(getattr(obj, 'plate_image', None), self.context.get('request'))
|
||
|
||
|
||
class PlateOrderByStateStatusSerializer(PlateOrderListV1Serializer):
|
||
"""按单个节点状态过滤 PlateOrder,并扩展状态流信息"""
|
||
|
||
state_parameters = serializers.SerializerMethodField()
|
||
state_log = serializers.SerializerMethodField()
|
||
state_status = serializers.SerializerMethodField()
|
||
|
||
class Meta(PlateOrderListV1Serializer.Meta):
|
||
fields = list(PlateOrderListV1Serializer.Meta.fields) + [
|
||
'state_status',
|
||
'state_parameters',
|
||
'state_log',
|
||
]
|
||
read_only_fields = list(PlateOrderListV1Serializer.Meta.read_only_fields) + [
|
||
'state_status',
|
||
'state_parameters',
|
||
'state_log',
|
||
]
|
||
|
||
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,
|
||
}
|
||
|
||
def to_representation(self, instance):
|
||
data = super().to_representation(instance)
|
||
plate_image = data.get('plate_image')
|
||
if isinstance(plate_image, list):
|
||
data['plate_image'] = plate_image[:1]
|
||
return data
|
||
|
||
|
||
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,
|
||
})
|
||
|
||
|
||
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',
|
||
)
|
||
)
|
||
|
||
if status_value == 'completed':
|
||
latest_completed_state_subquery = (
|
||
stateflow_models.StateFlowRecord.objects
|
||
.filter(
|
||
business_object_id=OuterRef('business_object_id'),
|
||
is_cancelled=False,
|
||
)
|
||
.order_by('-completed_at', '-id')
|
||
)
|
||
queryset = queryset.annotate(
|
||
latest_completed_state_id=Subquery(
|
||
latest_completed_state_subquery.values('state_id')[:1]
|
||
)
|
||
).filter(latest_completed_state_id=state_id_int)
|
||
|
||
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",
|
||
"design_code",
|
||
"plate_type",
|
||
"plate_date",
|
||
"plate_method",
|
||
"image_name",
|
||
"plate_notes",
|
||
"reprint_reason",
|
||
"urgency_level",
|
||
"is_invalid",
|
||
"customer",
|
||
"area",
|
||
"default_address",
|
||
"salesperson",
|
||
"merchandiser",
|
||
"designer",
|
||
"style_name",
|
||
"fabric",
|
||
"fabric_source",
|
||
"width",
|
||
"production_method",
|
||
"is_mark_frame",
|
||
"drawing_rating",
|
||
"color_matching_rating",
|
||
"sample_rating",
|
||
"difficulty_rating",
|
||
"sample_meter",
|
||
"required_sample_meters",
|
||
"required_completion_date",
|
||
"completion_date",
|
||
"approval_result",
|
||
"is_ordered",
|
||
"customer_feedback",
|
||
]
|
||
|
||
|
||
class PlateOrderBatchUpdateDataSerializer(serializers.ModelSerializer):
|
||
"""
|
||
PlateOrder 批量更新允许字段(白名单)。
|
||
|
||
说明:
|
||
- 不允许更新 process/business_object/created_by 等会触发流程副作用或越权的字段
|
||
- plate_image 属于结构化字段(且 v1 有额外转换逻辑),暂不纳入批量更新,避免前端误用
|
||
"""
|
||
|
||
class Meta:
|
||
model = printing_models.PlateOrder
|
||
fields = PLATE_ORDER_BATCH_UPDATE_ALLOWED_FIELDS
|
||
|
||
|
||
class PlateOrderBatchUpdateRequestSerializer(serializers.Serializer):
|
||
plate_order_ids = serializers.ListField(
|
||
child=serializers.IntegerField(min_value=1),
|
||
allow_empty=False,
|
||
help_text="需要批量更新的 PlateOrder id 列表(同一组 data 会应用到所有 id)",
|
||
)
|
||
data = serializers.DictField(
|
||
child=serializers.JSONField(),
|
||
allow_empty=False,
|
||
help_text='需要更新的字段集合,例如 {"urgency_level": "加急", "designer": 123}',
|
||
)
|
||
dry_run = serializers.BooleanField(
|
||
required=False,
|
||
default=False,
|
||
help_text="仅校验与预览,不实际写库",
|
||
)
|
||
|
||
def validate_plate_order_ids(self, value):
|
||
# 去重保持稳定性(前端可能重复传)
|
||
deduped = list(dict.fromkeys(value))
|
||
if not deduped:
|
||
raise serializers.ValidationError("plate_order_ids 不能为空")
|
||
return deduped
|
||
|
||
def validate_data(self, value):
|
||
data_srz = PlateOrderBatchUpdateDataSerializer(data=value, partial=True)
|
||
data_srz.is_valid(raise_exception=True)
|
||
if not data_srz.validated_data:
|
||
raise serializers.ValidationError({"detail": "data 不能为空"})
|
||
return data_srz.validated_data
|
||
|
||
|
||
class PlateOrderBatchUpdateView(APIView):
|
||
"""
|
||
PlateOrder 批量更新(同一份 data 应用到多个 plate_order)。
|
||
|
||
POST /api/v2/plate-orders/batch-update/
|
||
Body:
|
||
{
|
||
"plate_order_ids": [1, 2, 3],
|
||
"data": {"urgency_level": "加急", "designer": 10},
|
||
"dry_run": false
|
||
}
|
||
|
||
规则:
|
||
- 全成功/全失败(任意 id 不存在直接 400,不做部分更新)
|
||
- 需要 printing.change_plateorder 权限
|
||
- 如包含 is_invalid:
|
||
- is_invalid=true 需要 printing.can_invalidate_plateorder
|
||
- is_invalid=false 需要 printing.can_activate_plateorder
|
||
"""
|
||
|
||
permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
|
||
|
||
def post(self, request):
|
||
# 注意:Django 会缓存 has_perm 结果(user._perm_cache)。
|
||
# 在测试中 APIClient.force_authenticate 会复用同一个 user 实例,
|
||
# 可能导致“中途赋权后第二次请求仍判定无权限”的假阴性;这里主动清理缓存更稳妥。
|
||
for cache_attr in ("_perm_cache", "_user_perm_cache", "_group_perm_cache"):
|
||
if hasattr(request.user, cache_attr):
|
||
delattr(request.user, cache_attr)
|
||
|
||
if not request.user.has_perm("printing.change_plateorder"):
|
||
return Response({"detail": "您没有权限批量更新开版订单"}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 先对 data 做“字段白名单”校验(保证错误输出为顶层 detail,便于前端/测试消费)
|
||
raw_data = request.data.get("data") if isinstance(request.data, dict) else None
|
||
if isinstance(raw_data, dict):
|
||
allowed = set(PLATE_ORDER_BATCH_UPDATE_ALLOWED_FIELDS)
|
||
unknown = sorted(set(raw_data.keys()) - allowed)
|
||
if unknown:
|
||
return Response(
|
||
{"detail": f"不支持批量更新字段: {', '.join(unknown)}", "allowed_fields": sorted(allowed)},
|
||
status=status.HTTP_400_BAD_REQUEST,
|
||
)
|
||
|
||
srz = PlateOrderBatchUpdateRequestSerializer(data=request.data)
|
||
srz.is_valid(raise_exception=True)
|
||
payload = srz.validated_data
|
||
|
||
plate_order_ids: list[int] = payload["plate_order_ids"]
|
||
data: dict = payload["data"]
|
||
dry_run: bool = payload.get("dry_run", False)
|
||
|
||
# is_invalid 权限语义:沿用 v1 的作废/恢复权限
|
||
if "is_invalid" in data:
|
||
if data["is_invalid"] is True and not request.user.has_perm("printing.can_invalidate_plateorder"):
|
||
return Response({"detail": "您没有权限作废开版订单"}, status=status.HTTP_403_FORBIDDEN)
|
||
if data["is_invalid"] is False and not request.user.has_perm("printing.can_activate_plateorder"):
|
||
return Response({"detail": "您没有权限恢复开版订单"}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
qs = printing_models.PlateOrder.objects.filter(id__in=plate_order_ids)
|
||
found_ids = list(qs.values_list("id", flat=True))
|
||
found_set = set(found_ids)
|
||
missing_ids = [str(i) for i in plate_order_ids if i not in found_set]
|
||
if missing_ids:
|
||
return Response({"detail": f"以下 PlateOrder 不存在: {', '.join(missing_ids)}"}, status=status.HTTP_400_BAD_REQUEST)
|
||
# 返回/展示时保持与入参一致的顺序
|
||
found_ids = [i for i in plate_order_ids if i in found_set]
|
||
|
||
# 将 validated_data 转换为 queryset.update 可用的 kwargs(处理 FK -> *_id)
|
||
update_kwargs: dict = {}
|
||
for k, v in data.items():
|
||
try:
|
||
field = printing_models.PlateOrder._meta.get_field(k)
|
||
except Exception:
|
||
update_kwargs[k] = v
|
||
continue
|
||
|
||
if isinstance(field, django_models.ForeignKey):
|
||
update_kwargs[f"{k}_id"] = v.pk if v is not None else None
|
||
else:
|
||
update_kwargs[k] = v
|
||
|
||
update_kwargs["updated_at"] = timezone.now()
|
||
|
||
if dry_run:
|
||
return Response(
|
||
{
|
||
"dry_run": True,
|
||
"plate_order_ids": found_ids,
|
||
"matched_count": len(found_ids),
|
||
"data": request.data.get("data") or {},
|
||
}
|
||
)
|
||
|
||
with transaction.atomic():
|
||
updated_count = qs.update(**update_kwargs)
|
||
|
||
return Response(
|
||
{
|
||
"detail": "批量更新成功",
|
||
"updated_count": updated_count,
|
||
"plate_order_ids": found_ids,
|
||
}
|
||
)
|
||
|
||
|
||
class BatchAdvanceRecordSerializer(serializers.ModelSerializer):
|
||
"""批量操作记录序列化器"""
|
||
|
||
state_id = serializers.IntegerField(source='state.id', read_only=True)
|
||
state_name = serializers.CharField(source='state.name', read_only=True)
|
||
created_by_username = serializers.CharField(source='created_by.username', read_only=True, allow_null=True)
|
||
created_by_name = serializers.SerializerMethodField()
|
||
printing_job_ids = serializers.SerializerMethodField()
|
||
printing_job_count = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = printing_models.PrintingJobBatchAdvanceRecord
|
||
fields = [
|
||
'id',
|
||
'printing_order',
|
||
'state',
|
||
'state_id',
|
||
'state_name',
|
||
'created_by',
|
||
'created_by_username',
|
||
'created_by_name',
|
||
'parameters',
|
||
'only_parameters',
|
||
'printing_job_ids',
|
||
'printing_job_count',
|
||
'created_at',
|
||
]
|
||
read_only_fields = fields
|
||
|
||
def get_created_by_name(self, obj):
|
||
"""获取操作人员工姓名"""
|
||
user = getattr(obj, 'created_by', None)
|
||
if not user:
|
||
return None
|
||
emp = getattr(user, 'employee', None)
|
||
return getattr(emp, 'name', None)
|
||
|
||
def get_printing_job_ids(self, obj):
|
||
"""获取涉及的印染明细 ID 列表"""
|
||
# 使用预取的数据避免 N+1
|
||
if hasattr(obj, '_prefetched_objects_cache') and 'printing_jobs' in obj._prefetched_objects_cache:
|
||
return [job.id for job in obj.printing_jobs.all()]
|
||
return list(obj.printing_jobs.values_list('id', flat=True))
|
||
|
||
def get_printing_job_count(self, obj):
|
||
"""获取涉及的印染明细数量"""
|
||
if hasattr(obj, '_prefetched_objects_cache') and 'printing_jobs' in obj._prefetched_objects_cache:
|
||
return len(obj.printing_jobs.all())
|
||
return obj.printing_jobs.count()
|
||
|
||
|
||
class PrintingOrderBatchAdvanceRecordsView(APIView):
|
||
"""
|
||
获取印染订单的批量推进记录
|
||
|
||
根据 printing_order_id 查询该订单下所有的批量推进审计记录。
|
||
|
||
GET /api/v2/printing-orders/{printing_order_id}/batch-advance-records/
|
||
|
||
响应示例:
|
||
{
|
||
"printing_order_id": 55,
|
||
"records": [
|
||
{
|
||
"id": 9,
|
||
"printing_order": 55,
|
||
"state": 7,
|
||
"state_id": 7,
|
||
"state_name": "染色",
|
||
"created_by": 1001,
|
||
"created_by_username": "factory_user",
|
||
"created_by_name": "张三",
|
||
"parameters": {"temperature": "25.5"},
|
||
"printing_job_ids": [101, 102, 103],
|
||
"printing_job_count": 3,
|
||
"created_at": "2025-12-14T10:00:00+08:00"
|
||
}
|
||
],
|
||
"total_count": 1
|
||
}
|
||
"""
|
||
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
def get(self, request, printing_order_id):
|
||
# 验证订单是否存在
|
||
try:
|
||
printing_order = printing_models.PrintingOrder.objects.get(id=printing_order_id)
|
||
except printing_models.PrintingOrder.DoesNotExist:
|
||
return Response(
|
||
{'error': f'未找到 ID 为 {printing_order_id} 的印染订单'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# 查询批量推进记录,按创建时间倒序
|
||
records = printing_models.PrintingJobBatchAdvanceRecord.objects.filter(
|
||
printing_order_id=printing_order_id
|
||
).select_related(
|
||
'state',
|
||
'created_by',
|
||
'created_by__employee',
|
||
).prefetch_related(
|
||
'printing_jobs',
|
||
).order_by('-created_at')
|
||
|
||
serializer = BatchAdvanceRecordSerializer(records, many=True)
|
||
|
||
return Response({
|
||
'printing_order_id': printing_order_id,
|
||
'records': serializer.data,
|
||
'total_count': records.count(),
|
||
})
|
||
|
||
|
||
class PrintingOrderExternalSnapshotSyncRequestSerializer(serializers.Serializer):
|
||
external_order_id = serializers.CharField(max_length=100, help_text='外部订单编号')
|
||
allow_reset_stateflow = serializers.BooleanField(
|
||
required=False,
|
||
default=False,
|
||
help_text='是否允许先撤销目标订单下已有的所有工序,再执行覆盖同步',
|
||
)
|
||
|
||
def validate_external_order_id(self, value):
|
||
normalized = str(value or '').strip()
|
||
if not normalized:
|
||
raise serializers.ValidationError('external_order_id 不能为空')
|
||
return normalized
|
||
|
||
|
||
class PrintingOrderExternalSnapshotSyncView(APIView):
|
||
"""
|
||
按 external_order_id 触发外部订单快照同步。
|
||
|
||
行为:
|
||
- 若目标订单任意 printing_job 已关联销售品,则立即失败并记录审计
|
||
- 若目标订单存在已执行工序,默认失败;可通过 allow_reset_stateflow=true 先撤销全部工序后再覆盖
|
||
- 成功时复用现有外部订单映射逻辑覆盖 PrintingOrder / PrintingJob
|
||
"""
|
||
|
||
permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
|
||
|
||
def post(self, request):
|
||
srz = PrintingOrderExternalSnapshotSyncRequestSerializer(data=request.data)
|
||
srz.is_valid(raise_exception=True)
|
||
|
||
payload = srz.validated_data
|
||
try:
|
||
result = sync_external_printing_order_snapshot_impl(
|
||
external_order_id=payload['external_order_id'],
|
||
operator_user=request.user,
|
||
allow_reset_stateflow=payload.get('allow_reset_stateflow', False),
|
||
)
|
||
except ExternalPrintingOrderSnapshotSyncError as exc:
|
||
return Response(
|
||
{
|
||
'detail': str(exc),
|
||
'audit_id': exc.audit_id,
|
||
'external_order_id': payload['external_order_id'],
|
||
},
|
||
status=exc.status_code,
|
||
)
|
||
|
||
return Response(
|
||
{
|
||
'detail': '外部订单快照同步成功',
|
||
**result,
|
||
},
|
||
status=status.HTTP_200_OK,
|
||
)
|
||
|
||
|
||
class PrintingJobBatchAddParametersRequestSerializer(serializers.Serializer):
|
||
"""批量补充参数请求序列化器"""
|
||
printing_job_ids = serializers.ListField(
|
||
child=serializers.IntegerField(),
|
||
min_length=1,
|
||
help_text='印染明细ID列表'
|
||
)
|
||
state_id = serializers.IntegerField(help_text='目标流程节点ID')
|
||
parameters = serializers.DictField(
|
||
child=serializers.CharField(allow_blank=True),
|
||
help_text='要补充的工艺参数'
|
||
)
|
||
remark = serializers.CharField(required=False, default='', allow_blank=True, help_text='备注说明')
|
||
|
||
|
||
def _validate_jobs_for_batch_add_parameters(printing_job_ids: list[int], state_id: int) -> dict:
|
||
"""
|
||
验证批量补充参数的 jobs 并返回相关数据
|
||
|
||
验证规则:
|
||
1. 所有 job 必须存在
|
||
2. 所有 job 必须属于同一个 printing_order
|
||
3. 所有 job 必须有 business_object
|
||
4. 所有 job 的 business_object 必须有对应 state_id 的 state_log(未撤销)
|
||
|
||
返回:
|
||
{
|
||
'jobs': QuerySet[PrintingJob],
|
||
'printing_order': PrintingOrder,
|
||
'printing_order_id': int,
|
||
'state': State,
|
||
'state_logs': dict[int, StateFlowRecord], # job_id -> state_log
|
||
}
|
||
"""
|
||
from stateflow import models as stateflow_models
|
||
|
||
jobs = (
|
||
printing_models.PrintingJob.objects
|
||
.select_related('printing_order', 'business_object')
|
||
.filter(id__in=printing_job_ids)
|
||
)
|
||
|
||
found_ids = set(jobs.values_list('id', flat=True))
|
||
missing_ids = [i for i in printing_job_ids if i not in found_ids]
|
||
if missing_ids:
|
||
raise serializers.ValidationError({
|
||
'detail': f'以下 PrintingJob 不存在: {missing_ids}',
|
||
'missing_jobs': missing_ids,
|
||
})
|
||
|
||
# 验证同一订单
|
||
order_ids = set(jobs.values_list('printing_order_id', flat=True))
|
||
if len(order_ids) != 1:
|
||
raise serializers.ValidationError({
|
||
'detail': '所有 PrintingJob 必须属于同一个 PrintingOrder',
|
||
})
|
||
|
||
printing_order_id = order_ids.pop()
|
||
printing_order = printing_models.PrintingOrder.objects.get(id=printing_order_id)
|
||
|
||
# 验证 state 存在
|
||
try:
|
||
state = stateflow_models.State.objects.get(id=state_id)
|
||
except stateflow_models.State.DoesNotExist:
|
||
raise serializers.ValidationError({
|
||
'detail': f'State ID {state_id} 不存在',
|
||
})
|
||
|
||
# 验证所有 job 有 business_object 和对应的 state_log
|
||
jobs_without_bo = []
|
||
jobs_without_state_log = []
|
||
state_logs = {} # job_id -> state_log
|
||
|
||
for job in jobs:
|
||
if not job.business_object:
|
||
jobs_without_bo.append(job.id)
|
||
continue
|
||
|
||
# 查找对应 state_id 的 state_log(未撤销)
|
||
state_log = job.business_object.state_logs.filter(
|
||
state_id=state_id,
|
||
is_cancelled=False
|
||
).first()
|
||
|
||
if not state_log:
|
||
jobs_without_state_log.append(job.id)
|
||
else:
|
||
state_logs[job.id] = state_log
|
||
|
||
if jobs_without_bo:
|
||
raise serializers.ValidationError({
|
||
'detail': f'以下 PrintingJob 没有流程实例 (business_object): {jobs_without_bo}',
|
||
'missing_business_object': jobs_without_bo,
|
||
})
|
||
|
||
if jobs_without_state_log:
|
||
raise serializers.ValidationError({
|
||
'detail': f'以下 PrintingJob 没有对应 state_id={state_id} 的状态流转记录(或已撤销): {jobs_without_state_log}',
|
||
'missing_state_log': jobs_without_state_log,
|
||
})
|
||
|
||
return {
|
||
'jobs': jobs,
|
||
'printing_order': printing_order,
|
||
'printing_order_id': printing_order_id,
|
||
'state': state,
|
||
'state_logs': state_logs,
|
||
}
|
||
|
||
|
||
class PrintingJobBatchAddParametersView(APIView):
|
||
"""
|
||
批量补充工艺参数
|
||
|
||
为多个 PrintingJob 的已完成状态流转记录批量补充工艺参数。
|
||
|
||
POST /api/v2/printing-jobs/batch-add-parameters/
|
||
|
||
请求体:
|
||
{
|
||
"printing_job_ids": [101, 102, 103],
|
||
"state_id": 7,
|
||
"parameters": {
|
||
"temperature": "26.0",
|
||
"operator": "李四"
|
||
},
|
||
"remark": "批量补充参数备注"
|
||
}
|
||
|
||
响应:
|
||
{
|
||
"detail": "批量补充参数成功",
|
||
"batch_record_id": 15,
|
||
"printing_order_id": 55,
|
||
"printing_job_ids": [101, 102, 103],
|
||
"state_id": 7,
|
||
"state_name": "染色",
|
||
"parameters": {...},
|
||
"affected_count": 3
|
||
}
|
||
"""
|
||
|
||
permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
|
||
|
||
def post(self, request):
|
||
srz = PrintingJobBatchAddParametersRequestSerializer(data=request.data)
|
||
srz.is_valid(raise_exception=True)
|
||
|
||
payload = srz.validated_data
|
||
printing_job_ids = payload['printing_job_ids']
|
||
state_id = payload['state_id']
|
||
parameters = payload['parameters']
|
||
remark = payload.get('remark', '')
|
||
|
||
if not parameters:
|
||
raise serializers.ValidationError({
|
||
'detail': '参数不能为空',
|
||
})
|
||
|
||
# 验证数据
|
||
data = _validate_jobs_for_batch_add_parameters(printing_job_ids, state_id)
|
||
|
||
from stateflow import services as stateflow_services
|
||
|
||
# 全成功/全失败
|
||
with transaction.atomic():
|
||
# 创建批量操作记录
|
||
record = printing_models.PrintingJobBatchAdvanceRecord.objects.create(
|
||
printing_order=data['printing_order'],
|
||
state=data['state'],
|
||
created_by=request.user,
|
||
parameters=parameters,
|
||
only_parameters=True, # 标记为仅补充参数
|
||
)
|
||
record.printing_jobs.set(data['jobs'])
|
||
|
||
# 逐个补充参数
|
||
for job in data['jobs']:
|
||
state_log = data['state_logs'][job.id]
|
||
stateflow_services.add_parameters_to_state_log(
|
||
state_log,
|
||
remark=remark,
|
||
**parameters
|
||
)
|
||
|
||
return Response({
|
||
'detail': '批量补充参数成功',
|
||
'batch_record_id': record.id,
|
||
'printing_order_id': data['printing_order_id'],
|
||
'printing_job_ids': [j.id for j in data['jobs']],
|
||
'state_id': state_id,
|
||
'state_name': data['state'].name,
|
||
'parameters': parameters,
|
||
'affected_count': len(data['jobs']),
|
||
})
|