diff --git a/api_v2/tests.py b/api_v2/tests.py index 9cb8090..cd489cd 100644 --- a/api_v2/tests.py +++ b/api_v2/tests.py @@ -747,22 +747,16 @@ class PlateOrderByProcessNodeV2APITest(TestCase): 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']] - self.assertEqual(set(ids), {self.po_state2_old.id, self.po_state2_new.id}) + self.assertEqual(set(ids), {self.po_state3.id}) # 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]) + self.assertIn('created_by', by_id[self.po_state3.id]) + self.assertIn('process_parameters', by_id[self.po_state3.id]) - # po_state2_old:来自已撤销的 state2 提交记录 + # po_state3:已完成 state2,但未提交参数,value 应为 null self.assertTrue( - any(p['key'] == 'temperature' and p['value'] == '30' for p in by_id[self.po_state2_old.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']) + any(p['key'] == 'temperature' and p['value'] is None for p in by_id[self.po_state3.id]['process_parameters']) ) 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}) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.data['parameters'], []) - self.assertEqual(len(resp.data['results']), 1) - self.assertEqual(resp.data['results'][0]['id'], self.po_state1.id) - self.assertEqual(resp.data['results'][0]['process_parameters'], []) + self.assertEqual(len(resp.data['results']), 3) + ids = {item['id'] for item in resp.data['results']} + 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): resp = self.client.get(self.url, {'process_node_id': self.node2.id}) self.assertEqual(resp.status_code, 200) ids = [item['id'] for item in resp.data['results']] - self.assertEqual(ids[0], self.po_state2_new.id) - self.assertEqual(ids[1], self.po_state2_old.id) + self.assertEqual(ids[0], self.po_state3.id) def test_ordering_param_created_at_asc(self): resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'ordering': 'created_at'}) self.assertEqual(resp.status_code, 200) ids = [item['id'] for item in resp.data['results']] - self.assertEqual(ids[0], self.po_state2_old.id) - self.assertEqual(ids[1], self.po_state2_new.id) + self.assertEqual(ids[0], self.po_state3.id) def test_search_supports_design_code_icontains(self): resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'search': 'abc'}) self.assertEqual(resp.status_code, 200) 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): 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) 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): 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.data['count'], 2) + self.assertEqual(resp.data['count'], 1) self.assertEqual(len(resp.data['results']), 1) 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['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): def setUp(self): diff --git a/api_v2/views/printing.py b/api_v2/views/printing.py index 98e21b7..4e42318 100644 --- a/api_v2/views/printing.py +++ b/api_v2/views/printing.py @@ -486,10 +486,14 @@ class PlateOrderByProcessNodeView(APIView): 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) @@ -505,6 +509,22 @@ class PlateOrderByProcessNodeView(APIView): 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 @@ -525,7 +545,10 @@ class PlateOrderByProcessNodeView(APIView): .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: queryset = queryset.annotate( completed_prev_states=Count( @@ -547,7 +570,7 @@ class PlateOrderByProcessNodeView(APIView): ), distinct=True, ) - ).filter(completed_target=0) + ).filter(completed_target__gt=0) if param_key and param_value: matching_logs = stateflow_models.StateFlowRecord.objects.filter( diff --git a/docs/api_v2_plate_orders_by_process_node_2026-02-06.md b/docs/api_v2_plate_orders_by_process_node_2026-02-06.md index 5026497..9f368f9 100644 --- a/docs/api_v2_plate_orders_by_process_node_2026-02-06.md +++ b/docs/api_v2_plate_orders_by_process_node_2026-02-06.md @@ -15,6 +15,8 @@ - search:string - 纯数字:同时匹配 id 精确 + design_code 模糊 - 非纯数字:design_code 模糊 +- date_from:string,YYYY-MM-DD(与 date_to 成对提供) +- date_to:string,YYYY-MM-DD(与 date_from 成对提供) - ordering:string,默认 -created_at - 可选字段:id / created_at / updated_at / design_code - limit:int,分页大小(默认 20) @@ -79,6 +81,5 @@ ## 备注(查询流程) - 起点为 PlateOrder 查询,再通过 business_object 的状态日志进行筛选。 -- “当前处于该节点”的判定采用 NEXT 模式: - - 目标节点前置节点全部完成(未撤销) - - 目标节点尚未完成(未撤销) +- “当前已完成”模式判定: + - 目标节点已完成(未撤销)