forked from erp-dev/erp
fix: business.object_id maybe null, missing parameters when plate-Order cloned
This commit is contained in:
290
api_v2/tests.py
290
api_v2/tests.py
@@ -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_exec:state1 已执行;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)
|
||||
|
||||
Reference in New Issue
Block a user