forked from erp-dev/erp
fix: by-process-node do not filter current state and added datr_from + date_to
This commit is contained in:
@@ -747,22 +747,16 @@ class PlateOrderByProcessNodeV2APITest(TestCase):
|
|||||||
self.assertTrue(any(p['key'] == 'temperature' and p['value'] == '25' for p in resp.data['parameters']))
|
self.assertTrue(any(p['key'] == 'temperature' and p['value'] == '25' for p in resp.data['parameters']))
|
||||||
|
|
||||||
ids = [item['id'] for item in resp.data['results']]
|
ids = [item['id'] for item in resp.data['results']]
|
||||||
self.assertEqual(set(ids), {self.po_state2_old.id, self.po_state2_new.id})
|
self.assertEqual(set(ids), {self.po_state3.id})
|
||||||
|
|
||||||
# results 必须包含“订单维度”的参数值(同一节点下不同订单可不同)
|
# results 必须包含“订单维度”的参数值(同一节点下不同订单可不同)
|
||||||
by_id = {item['id']: item for item in resp.data['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_state3.id])
|
||||||
self.assertIn('created_by', by_id[self.po_state2_new.id])
|
self.assertIn('process_parameters', by_id[self.po_state3.id])
|
||||||
self.assertIn('process_parameters', by_id[self.po_state2_old.id])
|
|
||||||
self.assertIn('process_parameters', by_id[self.po_state2_new.id])
|
|
||||||
|
|
||||||
# po_state2_old:来自已撤销的 state2 提交记录
|
# po_state3:已完成 state2,但未提交参数,value 应为 null
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
any(p['key'] == 'temperature' and p['value'] == '30' for p in by_id[self.po_state2_old.id]['process_parameters'])
|
any(p['key'] == 'temperature' and p['value'] is None for p in by_id[self.po_state3.id]['process_parameters'])
|
||||||
)
|
|
||||||
# po_state2_new:从未提交过 state2 参数,value 应为 null
|
|
||||||
self.assertTrue(
|
|
||||||
any(p['key'] == 'temperature' and p['value'] is None for p in by_id[self.po_state2_new.id]['process_parameters'])
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_process_parameters_schema_and_keys_align_with_template(self):
|
def test_process_parameters_schema_and_keys_align_with_template(self):
|
||||||
@@ -788,40 +782,40 @@ class PlateOrderByProcessNodeV2APITest(TestCase):
|
|||||||
resp = self.client.get(self.url, {'process_node_id': self.node1.id})
|
resp = self.client.get(self.url, {'process_node_id': self.node1.id})
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(resp.data['parameters'], [])
|
self.assertEqual(resp.data['parameters'], [])
|
||||||
self.assertEqual(len(resp.data['results']), 1)
|
self.assertEqual(len(resp.data['results']), 3)
|
||||||
self.assertEqual(resp.data['results'][0]['id'], self.po_state1.id)
|
ids = {item['id'] for item in resp.data['results']}
|
||||||
self.assertEqual(resp.data['results'][0]['process_parameters'], [])
|
self.assertEqual(ids, {self.po_state2_old.id, self.po_state2_new.id, self.po_state3.id})
|
||||||
|
for item in resp.data['results']:
|
||||||
|
self.assertEqual(item['process_parameters'], [])
|
||||||
|
|
||||||
def test_default_ordering_is_minus_created_at(self):
|
def test_default_ordering_is_minus_created_at(self):
|
||||||
resp = self.client.get(self.url, {'process_node_id': self.node2.id})
|
resp = self.client.get(self.url, {'process_node_id': self.node2.id})
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
ids = [item['id'] for item in resp.data['results']]
|
ids = [item['id'] for item in resp.data['results']]
|
||||||
self.assertEqual(ids[0], self.po_state2_new.id)
|
self.assertEqual(ids[0], self.po_state3.id)
|
||||||
self.assertEqual(ids[1], self.po_state2_old.id)
|
|
||||||
|
|
||||||
def test_ordering_param_created_at_asc(self):
|
def test_ordering_param_created_at_asc(self):
|
||||||
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'ordering': 'created_at'})
|
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'ordering': 'created_at'})
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
ids = [item['id'] for item in resp.data['results']]
|
ids = [item['id'] for item in resp.data['results']]
|
||||||
self.assertEqual(ids[0], self.po_state2_old.id)
|
self.assertEqual(ids[0], self.po_state3.id)
|
||||||
self.assertEqual(ids[1], self.po_state2_new.id)
|
|
||||||
|
|
||||||
def test_search_supports_design_code_icontains(self):
|
def test_search_supports_design_code_icontains(self):
|
||||||
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'search': 'abc'})
|
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'search': 'abc'})
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
ids = [item['id'] for item in resp.data['results']]
|
ids = [item['id'] for item in resp.data['results']]
|
||||||
self.assertEqual(ids, [self.po_state2_new.id])
|
self.assertEqual(ids, [])
|
||||||
|
|
||||||
def test_search_supports_pk_without_extra_param(self):
|
def test_search_supports_pk_without_extra_param(self):
|
||||||
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'search': str(self.po_state2_old.id)})
|
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'search': str(self.po_state2_old.id)})
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
ids = [item['id'] for item in resp.data['results']]
|
ids = [item['id'] for item in resp.data['results']]
|
||||||
self.assertEqual(ids, [self.po_state2_old.id])
|
self.assertEqual(ids, [])
|
||||||
|
|
||||||
def test_pagination_limit_offset(self):
|
def test_pagination_limit_offset(self):
|
||||||
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'limit': 1, 'offset': 0})
|
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'limit': 1, 'offset': 0})
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(resp.data['count'], 2)
|
self.assertEqual(resp.data['count'], 1)
|
||||||
self.assertEqual(len(resp.data['results']), 1)
|
self.assertEqual(len(resp.data['results']), 1)
|
||||||
|
|
||||||
def test_param_filter_requires_both_key_and_value(self):
|
def test_param_filter_requires_both_key_and_value(self):
|
||||||
@@ -846,6 +840,43 @@ class PlateOrderByProcessNodeV2APITest(TestCase):
|
|||||||
self.assertEqual(resp.data['count'], 0)
|
self.assertEqual(resp.data['count'], 0)
|
||||||
self.assertEqual(resp.data['results'], [])
|
self.assertEqual(resp.data['results'], [])
|
||||||
|
|
||||||
|
def test_date_filter_requires_both(self):
|
||||||
|
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'date_from': '2026-02-01'})
|
||||||
|
self.assertEqual(resp.status_code, 400)
|
||||||
|
self.assertIn('date_from', resp.data.get('detail', ''))
|
||||||
|
|
||||||
|
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'date_to': '2026-02-01'})
|
||||||
|
self.assertEqual(resp.status_code, 400)
|
||||||
|
self.assertIn('date_to', resp.data.get('detail', ''))
|
||||||
|
|
||||||
|
def test_date_filter_applies(self):
|
||||||
|
tz = timezone.get_default_timezone()
|
||||||
|
target_dt = timezone.make_aware(datetime.datetime(2025, 12, 3, 10, 0, 0), tz)
|
||||||
|
printing_models.PlateOrder.objects.filter(id=self.po_state3.id).update(created_at=target_dt)
|
||||||
|
|
||||||
|
resp = self.client.get(
|
||||||
|
self.url,
|
||||||
|
{
|
||||||
|
'process_node_id': self.node2.id,
|
||||||
|
'date_from': '2025-12-03',
|
||||||
|
'date_to': '2025-12-03',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
ids = [item['id'] for item in resp.data['results']]
|
||||||
|
self.assertEqual(ids, [self.po_state3.id])
|
||||||
|
|
||||||
|
resp = self.client.get(
|
||||||
|
self.url,
|
||||||
|
{
|
||||||
|
'process_node_id': self.node2.id,
|
||||||
|
'date_from': '2025-12-04',
|
||||||
|
'date_to': '2025-12-04',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertEqual(resp.data['results'], [])
|
||||||
|
|
||||||
|
|
||||||
class PlateOrderByProcessV2APITest(TestCase):
|
class PlateOrderByProcessV2APITest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -486,10 +486,14 @@ class PlateOrderByProcessNodeView(APIView):
|
|||||||
process_node_id = qp.get('process_node_id')
|
process_node_id = qp.get('process_node_id')
|
||||||
param_key = (qp.get('param_key') or '').strip()
|
param_key = (qp.get('param_key') or '').strip()
|
||||||
param_value = (qp.get('param_value') 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:
|
if not process_node_id:
|
||||||
return Response({'detail': 'process_node_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
|
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):
|
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)
|
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:
|
try:
|
||||||
process_node_id_int = int(process_node_id)
|
process_node_id_int = int(process_node_id)
|
||||||
@@ -505,6 +509,22 @@ class PlateOrderByProcessNodeView(APIView):
|
|||||||
except stateflow_models.ProcessNode.DoesNotExist:
|
except stateflow_models.ProcessNode.DoesNotExist:
|
||||||
return Response({'detail': 'process_node 不存在'}, status=status.HTTP_404_NOT_FOUND)
|
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_state_id = process_node.state_id
|
||||||
target_order = process_node.order
|
target_order = process_node.order
|
||||||
@@ -525,7 +545,10 @@ class PlateOrderByProcessNodeView(APIView):
|
|||||||
.filter(business_object__isnull=False, business_object__process_id=process_id)
|
.filter(business_object__isnull=False, business_object__process_id=process_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# NEXT 模式判定:前置节点都已完成(未撤销) + 目标节点尚未完成(未撤销)
|
if date_from and date_to:
|
||||||
|
queryset = queryset.filter(created_at__gte=start_dt, created_at__lte=end_dt)
|
||||||
|
|
||||||
|
# COMPLETED 模式判定:目标节点已完成(未撤销)
|
||||||
if prev_state_ids:
|
if prev_state_ids:
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
completed_prev_states=Count(
|
completed_prev_states=Count(
|
||||||
@@ -547,7 +570,7 @@ class PlateOrderByProcessNodeView(APIView):
|
|||||||
),
|
),
|
||||||
distinct=True,
|
distinct=True,
|
||||||
)
|
)
|
||||||
).filter(completed_target=0)
|
).filter(completed_target__gt=0)
|
||||||
|
|
||||||
if param_key and param_value:
|
if param_key and param_value:
|
||||||
matching_logs = stateflow_models.StateFlowRecord.objects.filter(
|
matching_logs = stateflow_models.StateFlowRecord.objects.filter(
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
- search:string
|
- search:string
|
||||||
- 纯数字:同时匹配 id 精确 + design_code 模糊
|
- 纯数字:同时匹配 id 精确 + design_code 模糊
|
||||||
- 非纯数字:design_code 模糊
|
- 非纯数字:design_code 模糊
|
||||||
|
- date_from:string,YYYY-MM-DD(与 date_to 成对提供)
|
||||||
|
- date_to:string,YYYY-MM-DD(与 date_from 成对提供)
|
||||||
- ordering:string,默认 -created_at
|
- ordering:string,默认 -created_at
|
||||||
- 可选字段:id / created_at / updated_at / design_code
|
- 可选字段:id / created_at / updated_at / design_code
|
||||||
- limit:int,分页大小(默认 20)
|
- limit:int,分页大小(默认 20)
|
||||||
@@ -79,6 +81,5 @@
|
|||||||
|
|
||||||
## 备注(查询流程)
|
## 备注(查询流程)
|
||||||
- 起点为 PlateOrder 查询,再通过 business_object 的状态日志进行筛选。
|
- 起点为 PlateOrder 查询,再通过 business_object 的状态日志进行筛选。
|
||||||
- “当前处于该节点”的判定采用 NEXT 模式:
|
- “当前已完成”模式判定:
|
||||||
- 目标节点前置节点全部完成(未撤销)
|
- 目标节点已完成(未撤销)
|
||||||
- 目标节点尚未完成(未撤销)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user