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)