1
0
forked from erp-dev/erp

fix: business.object_id maybe null, missing parameters when plate-Order cloned

This commit is contained in:
2025-12-17 11:02:07 +08:00
parent 639efe8421
commit 51fad953c3
24 changed files with 13247 additions and 47 deletions

View File

@@ -352,6 +352,15 @@ class PrintingJobBatchAdvanceV2APITest(TestCase):
# stateflow两条 business_object 都应生成 1 条完成日志,且完成的是 state1
self.job1.refresh_from_db()
self.job2.refresh_from_db()
# 同时应修复绑定BusinessObject.content_type/object_id 指向 PrintingJob
from django.contrib.contenttypes.models import ContentType
ct_job = ContentType.objects.get_for_model(printing_models.PrintingJob)
self.job1.business_object.refresh_from_db()
self.job2.business_object.refresh_from_db()
self.assertEqual(self.job1.business_object.content_type_id, ct_job.id)
self.assertEqual(self.job1.business_object.object_id, self.job1.id)
self.assertEqual(self.job2.business_object.content_type_id, ct_job.id)
self.assertEqual(self.job2.business_object.object_id, self.job2.id)
self.assertEqual(self.job1.business_object.state_logs.filter(is_cancelled=False).count(), 1)
self.assertEqual(self.job2.business_object.state_logs.filter(is_cancelled=False).count(), 1)
self.assertEqual(self.job1.business_object.state_logs.first().state_id, self.state1.id)
@@ -395,12 +404,15 @@ class BusinessObjectCloneV2APITest(TestCase):
stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
self.process_src = stateflow_models.Process.objects.create(name='P-src')
self.process_dst = stateflow_models.Process.objects.create(name='P-dst')
ct = ContentType.objects.get_for_model(stateflow_models.Process)
self.bo = stateflow_models.BusinessObject.objects.create(
name='BO',
process=self.process,
content_type=ct,
object_id=111,
object_id=self.process_src.id,
)
# 创建一条日志 + 参数记录,确保“克隆包含工艺参数”
@@ -416,7 +428,7 @@ class BusinessObjectCloneV2APITest(TestCase):
)
def test_clone_business_object_api_returns_new_id(self):
new_object_id = 222
new_object_id = self.process_dst.id
resp = self.client.post(
'/api/v2/stateflow/business-objects/clone/',
{
@@ -438,6 +450,78 @@ class BusinessObjectCloneV2APITest(TestCase):
self.assertEqual(cloned.content_type_id, self.bo.content_type_id)
self.assertEqual(cloned.object_id, new_object_id)
def test_clone_business_object_api_binds_to_plate_order_business_object(self):
"""
printing 工作流依赖:新建 PlateOrder 后通过 clone API 复制流程进度,
后端应自动把克隆后的 BusinessObject 绑定到目标 PlateOrder.business_object。
"""
merchant = basic_models.Merchant.objects.create(
name='M1',
type=basic_models.MerchantTypeEnum.FACTORY,
)
customer = basic_models.Customer.objects.create(
merchant=merchant,
name='C1',
created_by=None,
)
# source PlateOrder会自动创建 business_object
source_po = printing_models.PlateOrder.objects.create(
customer=customer,
process=self.process.id,
design_code='SRC',
style_name='S',
)
self.assertIsNotNone(source_po.business_object_id)
# 给 source 写一条日志 + 参数,确保克隆后能看到“节点与参数”
log = stateflow_models.StateFlowRecord.objects.create(
business_object=source_po.business_object,
state=self.state1,
completed_by=self.user,
)
stateflow_models.StateLogParameterRecord.objects.create(
state_log=log,
parameters={'temperature': '25.5'},
remark='r1',
)
# target PlateOrder创建时也会自动创建一个“空 business_object”
target_po = printing_models.PlateOrder.objects.create(
customer=customer,
process=self.process.id,
design_code='DST',
style_name='D',
)
self.assertIsNotNone(target_po.business_object_id)
old_target_bo_id = target_po.business_object_id
ct_po = ContentType.objects.get_for_model(printing_models.PlateOrder)
resp = self.client.post(
'/api/v2/stateflow/business-objects/clone/',
{
'business_object_id': source_po.business_object_id,
'content_type': ct_po.id,
'object_id': target_po.id,
},
format='json'
)
self.assertEqual(resp.status_code, 200, resp.data)
new_bo_id = resp.data['business_object_id']
target_po.refresh_from_db()
self.assertEqual(target_po.business_object_id, new_bo_id)
# 目标 PlateOrder 的旧“空 BO”应被安全清理避免残留多份绑定
self.assertFalse(stateflow_models.BusinessObject.objects.filter(id=old_target_bo_id).exists())
# 克隆后的日志与参数应存在
self.assertEqual(target_po.business_object.state_logs.count(), 1)
cloned_log = target_po.business_object.state_logs.first()
self.assertEqual(cloned_log.state_id, self.state1.id)
self.assertEqual(cloned_log.parameter_records.count(), 1)
self.assertEqual(cloned_log.parameter_records.first().parameters.get('temperature'), '25.5')
def test_clone_business_object_api_404(self):
resp = self.client.post(
'/api/v2/stateflow/business-objects/clone/',
@@ -601,6 +685,8 @@ class PlateOrderByProcessNodeV2APITest(TestCase):
# results 必须包含“订单维度”的参数值(同一节点下不同订单可不同)
by_id = {item['id']: item for item in resp.data['results']}
self.assertIn('created_by', by_id[self.po_state2_old.id])
self.assertIn('created_by', by_id[self.po_state2_new.id])
self.assertIn('process_parameters', by_id[self.po_state2_old.id])
self.assertIn('process_parameters', by_id[self.po_state2_new.id])
@@ -671,3 +757,203 @@ class PlateOrderByProcessNodeV2APITest(TestCase):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['count'], 2)
self.assertEqual(len(resp.data['results']), 1)
class PlateOrderByProcessV2APITest(TestCase):
def setUp(self):
self.client = APIClient()
# 工厂用户(满足 IsPrintingFactory
self.merchant = basic_models.Merchant.objects.create(
name='开版工厂2',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.user = get_user_model().objects.create_user(username='plate_process_user', password='pass12345')
basic_models.Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='开版员工2',
)
self.client.force_authenticate(user=self.user)
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name='客户PP',
created_by=None,
)
# stateflow流程 + 3个节点其中 2 个节点有参数1 个无参数)
self.state1 = stateflow_models.State.objects.create(name='画图')
self.state2 = stateflow_models.State.objects.create(name='调色')
self.state3 = stateflow_models.State.objects.create(name='套样')
self.param_len = stateflow_models.StateParameter.objects.create(key='length', value=None)
self.param_temp = stateflow_models.StateParameter.objects.create(key='temperature', value=None)
self.state1.parameters.add(self.param_len)
self.state2.parameters.add(self.param_temp)
self.process = stateflow_models.Process.objects.create(name='开版流程2')
self.node1 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
self.node2 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
self.node3 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
# PlateOrder同一个 process不同 created_at
self.po_no_exec = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='CODE-X', # 不包含数字,便于测试 plate_order=pk
style_name='款式1',
)
self.po_exec = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='PO-EXEC',
style_name='款式2',
)
# 执行 state1非撤销并提交参数
log1 = stateflow_models.StateFlowRecord.objects.create(
business_object=self.po_exec.business_object,
state=self.state1,
completed_by=self.user,
is_cancelled=False,
)
stateflow_models.StateLogParameterRecord.objects.create(
state_log=log1,
parameters={'length': '5米'},
)
# 执行 state2撤销并提交参数默认不带撤销参数 -> 应视为未执行)
cancelled_log2 = stateflow_models.StateFlowRecord.objects.create(
business_object=self.po_exec.business_object,
state=self.state2,
completed_by=self.user,
is_cancelled=True,
)
stateflow_models.StateLogParameterRecord.objects.create(
state_log=cancelled_log2,
parameters={'temperature': '30'},
)
self.po_out_of_range = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='OUT',
style_name='款式3',
)
# 固定 created_at
tz = timezone.get_default_timezone()
t1 = timezone.make_aware(datetime.datetime(2025, 12, 1, 10, 0, 0), tz)
t2 = timezone.make_aware(datetime.datetime(2025, 12, 2, 10, 0, 0), tz)
t0 = timezone.make_aware(datetime.datetime(2025, 11, 20, 10, 0, 0), tz)
printing_models.PlateOrder.objects.filter(id=self.po_no_exec.id).update(created_at=t1)
printing_models.PlateOrder.objects.filter(id=self.po_exec.id).update(created_at=t2)
printing_models.PlateOrder.objects.filter(id=self.po_out_of_range.id).update(created_at=t0)
self.url = '/api/v2/plate-orders/by-process/'
def test_list_by_process_and_date_range(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'date_from': '2025-12-01',
'date_to': '2025-12-02',
}
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['process']['id'], self.process.id)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(set(ids), {self.po_no_exec.id, self.po_exec.id})
# 序列化器字段created_by 必须存在(允许为 null
for item in resp.data['results']:
self.assertIn('created_by', item)
def test_default_ordering_is_minus_created_at(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'date_from': '2025-12-01',
'date_to': '2025-12-02',
}
)
self.assertEqual(resp.status_code, 200)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(ids[0], self.po_exec.id)
self.assertEqual(ids[1], self.po_no_exec.id)
def test_plate_order_param_supports_design_code_icontains(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'date_from': '2025-12-01',
'date_to': '2025-12-02',
'plate_order': 'exec',
}
)
self.assertEqual(resp.status_code, 200)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(ids, [self.po_exec.id])
def test_plate_order_param_supports_pk_without_extra_param(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'date_from': '2025-12-01',
'date_to': '2025-12-02',
'plate_order': str(self.po_no_exec.id),
}
)
self.assertEqual(resp.status_code, 200)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(ids, [self.po_no_exec.id])
def test_process_params_includes_all_nodes_and_is_executed(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'date_from': '2025-12-01',
'date_to': '2025-12-02',
}
)
self.assertEqual(resp.status_code, 200)
by_id = {item['id']: item for item in resp.data['results']}
po_exec = by_id[self.po_exec.id]
po_no_exec = by_id[self.po_no_exec.id]
# 每条 PlateOrder 都必须有 process_params且包含所有节点按 order
self.assertIn('process_params', po_exec)
self.assertEqual([n['order'] for n in po_exec['process_params']], [0, 1, 2])
self.assertEqual([n['node_name'] for n in po_exec['process_params']], ['画图', '调色', '套样'])
# po_execstate1 已执行state2 只有撤销记录 -> 默认视为未执行state3 未执行
n1, n2, n3 = po_exec['process_params']
self.assertTrue(n1['is_executed'])
self.assertFalse(n2['is_executed'])
self.assertFalse(n3['is_executed'])
self.assertEqual([p['key'] for p in n1['params']], ['length'])
self.assertEqual(n1['params'][0]['value'], '5米')
self.assertEqual([p['key'] for p in n2['params']], ['temperature'])
self.assertIsNone(n2['params'][0]['value'])
self.assertEqual(n3['params'], [])
# po_no_exec全部未执行参数 value 全为 null
self.assertEqual([n['order'] for n in po_no_exec['process_params']], [0, 1, 2])
self.assertFalse(po_no_exec['process_params'][0]['is_executed'])
self.assertIsNone(po_no_exec['process_params'][0]['params'][0]['value'])
def test_invalid_ordering_returns_400(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'date_from': '2025-12-01',
'date_to': '2025-12-02',
'ordering': 'unknown',
}
)
self.assertEqual(resp.status_code, 400)

View File

@@ -6,6 +6,7 @@ from api_v2.views import (
PrintingJobBatchAdvancePreviewView,
PrintingJobBatchAdvanceSubmitView,
PlateOrderByProcessNodeView,
PlateOrderByProcessView,
BusinessObjectCloneView,
)
@@ -15,6 +16,7 @@ urlpatterns = [
path('printing-jobs/batch-advance/preview/', PrintingJobBatchAdvancePreviewView.as_view(), name='api_v2_printing_job_batch_advance_preview'),
path('printing-jobs/batch-advance/', PrintingJobBatchAdvanceSubmitView.as_view(), name='api_v2_printing_job_batch_advance_submit'),
path('plate-orders/by-process-node/', PlateOrderByProcessNodeView.as_view(), name='api_v2_plate_order_by_process_node'),
path('plate-orders/by-process/', PlateOrderByProcessView.as_view(), name='api_v2_plate_order_by_process'),
path('stateflow/business-objects/clone/', BusinessObjectCloneView.as_view(), name='api_v2_stateflow_business_object_clone'),
]

View File

@@ -9,6 +9,7 @@ from .printing import (
PrintingJobBatchAdvancePreviewView,
PrintingJobBatchAdvanceSubmitView,
PlateOrderByProcessNodeView,
PlateOrderByProcessView,
)
from .stateflow import BusinessObjectCloneView
@@ -19,6 +20,7 @@ __all__ = [
'PrintingJobBatchAdvancePreviewView',
'PrintingJobBatchAdvanceSubmitView',
'PlateOrderByProcessNodeView',
'PlateOrderByProcessView',
'BusinessObjectCloneView',
]

View File

@@ -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,
})

View File

@@ -1,6 +1,8 @@
from rest_framework import serializers, status, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from django.core.exceptions import FieldDoesNotExist
from django.db import transaction
from stateflow import models as stateflow_models
from stateflow import services as stateflow_services
@@ -49,11 +51,48 @@ class BusinessObjectCloneView(APIView):
return Response({'detail': 'business_object 不存在'}, status=status.HTTP_404_NOT_FOUND)
try:
cloned = stateflow_services.clone_business_object(
bo,
new_object_id=new_object_id,
expected_content_type_id=expected_content_type_id,
)
with transaction.atomic():
cloned = stateflow_services.clone_business_object(
bo,
new_object_id=new_object_id,
expected_content_type_id=expected_content_type_id,
)
# 兼容 printing 的“克隆=新建后复制流程进度”工作流:
# 目标业务对象PlateOrder/PrintingJob本身持有 business_object 字段,
# 需要把克隆后的 BusinessObject 回写到目标对象上,否则前端看不到节点/参数历史。
try:
target_obj = cloned.content_type.get_object_for_this_type(pk=cloned.object_id)
except Exception:
target_obj = None
if target_obj is not None:
try:
bo_field = target_obj._meta.get_field('business_object')
except FieldDoesNotExist:
bo_field = None
if bo_field is not None and bo_field.related_model is stateflow_models.BusinessObject:
old_bo_id = getattr(target_obj, 'business_object_id', None)
target_obj.business_object = cloned
target_obj.save(update_fields=['business_object'])
# 如果目标对象在创建时自动生成了一个“空 business_object”这里做安全清理
# 避免同一个业务对象存在多个 BusinessObject 绑定导致后续审计/排查混乱。
if old_bo_id and old_bo_id != cloned.id:
old_bo = (
stateflow_models.BusinessObject.objects
.filter(id=old_bo_id)
.select_related('content_type')
.first()
)
if (
old_bo
and old_bo.state_logs.count() == 0
and old_bo.content_type_id == cloned.content_type_id
and old_bo.object_id == cloned.object_id
):
old_bo.delete()
except ValueError as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)