From 1bb00b14f256419dbb3efa3a4de4148880ca095e Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 21 Sep 2026 22:49:59 +0200 Subject: [PATCH] fix(measure): riepilogo fine task filtrato per lotto/seriale/operatore, pagina oltre 500 (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit task_complete(): GET /api/measurements ora porta lot_number/serial_number (sessione) + measured_by (utente corrente), invece del solo version_id - una seconda produzione sulla stessa versione non pesca più le misure di altre stazioni/giorni. Risposte oltre 500 misure vengono cicliche per pagina invece di troncate al primo per_page=500. production_run_id sarebbe il filtro più preciso ma GET /api/measurements non lo accetta (lotto A3) e task_execute.html non lo passa in query (lotto B1): MANCA, annotato nel codice. Co-Authored-By: Claude Sonnet 5 --- src/frontend/flask_app/blueprints/measure.py | 64 ++++++++++++------ src/frontend/flask_app/tests/test_measure.py | 71 ++++++++++++++++++++ 2 files changed, 115 insertions(+), 20 deletions(-) diff --git a/src/frontend/flask_app/blueprints/measure.py b/src/frontend/flask_app/blueprints/measure.py index 10a33f2..9ca10aa 100644 --- a/src/frontend/flask_app/blueprints/measure.py +++ b/src/frontend/flask_app/blueprints/measure.py @@ -354,6 +354,8 @@ def task_complete(recipe_id: int): """Task completion summary with all measurements.""" # Retrieve version_id from query params version_id = request.args.get("version_id") + lot_number = session.get("lot_number", "") + serial_number = session.get("serial_number", "") # Load recipe for context recipe_resp = api_client.get(f"/api/recipes/{recipe_id}") @@ -382,30 +384,52 @@ def task_complete(recipe_id: int): # Load measurements if version_id provided measurements = [] if version_id: - meas_resp = api_client.get( - "/api/measurements", - params={"version_id": version_id, "per_page": 500}, - ) - if not (isinstance(meas_resp, dict) and meas_resp.get("error")): - raw = ( + # Scoped to this measurement session, not the whole version (#30): a + # second production run on the same recipe version used to pull in + # every other station's/day's readings too. GET /api/measurements has + # no production_run_id filter yet (that's lotto A3), so lot/serial + + # the current operator is the closest available identity - MANCA: + # once the backend accepts production_run_id, prefer it when the + # active run is known (see _current_run_id()). + params = {"version_id": version_id, "per_page": 500} + if lot_number: + params["lot_number"] = lot_number + if serial_number: + params["serial_number"] = serial_number + user_id = session.get("user_id") + if user_id: + params["measured_by"] = user_id + + raw = [] + page = 1 + while True: + meas_resp = api_client.get( + "/api/measurements", params={**params, "page": page}, + ) + if isinstance(meas_resp, dict) and meas_resp.get("error"): + break + page_items = ( meas_resp if isinstance(meas_resp, list) else meas_resp.get("items", []) ) - # Enrich each measurement with nested subtask data - for m in raw: - st = subtask_map.get(m.get("subtask_id"), {}) - m["subtask"] = st - m["task_info"] = subtask_task_map.get(m.get("subtask_id"), {}) - # Compute deviation if not present - if m.get("deviation") is None and st.get("nominal") is not None: - try: - m["deviation"] = m["value"] - st["nominal"] - except (TypeError, KeyError): - m["deviation"] = 0.0 - measurements = raw + raw.extend(page_items) + total_pages = meas_resp.get("pages", 1) if isinstance(meas_resp, dict) else 1 + if page >= total_pages or not page_items: + break + page += 1 - lot_number = session.get("lot_number", "") - serial_number = session.get("serial_number", "") + # Enrich each measurement with nested subtask data + for m in raw: + st = subtask_map.get(m.get("subtask_id"), {}) + m["subtask"] = st + m["task_info"] = subtask_task_map.get(m.get("subtask_id"), {}) + # Compute deviation if not present + if m.get("deviation") is None and st.get("nominal") is not None: + try: + m["deviation"] = m["value"] - st["nominal"] + except (TypeError, KeyError): + m["deviation"] = 0.0 + measurements = raw return render_template( "measure/task_complete.html", diff --git a/src/frontend/flask_app/tests/test_measure.py b/src/frontend/flask_app/tests/test_measure.py index 4dc5545..3539f35 100644 --- a/src/frontend/flask_app/tests/test_measure.py +++ b/src/frontend/flask_app/tests/test_measure.py @@ -264,6 +264,77 @@ class TestTaskComplete: assert resp.status_code == 200 assert b"productionClock(" in resp.data + def test_measurements_filtered_by_lot_serial_and_operator( + self, logged_in_client, mock_api_client, + ): + """#30: a second production on the same version must not pull in every + other station's/day's readings - only this lot/serial and operator.""" + with logged_in_client.session_transaction() as sess: + sess["lot_number"] = "L1" + sess["serial_number"] = "S1" + + mock_api_client.get.side_effect = [ + {"id": 1, "code": "REC-001", "name": "Test Recipe", "version": 1}, + [{"id": 1, "title": "Task 1", "order_index": 0, "subtasks": []}], + {"items": [], "pages": 1}, + ] + + resp = logged_in_client.get("/measure/complete/1?version_id=5") + assert resp.status_code == 200 + + endpoint, kwargs = mock_api_client.get.call_args_list[2] + assert endpoint[0] == "/api/measurements" + params = kwargs["params"] + assert params["version_id"] == "5" + assert params["lot_number"] == "L1" + assert params["serial_number"] == "S1" + assert params["measured_by"] == 1 # session["user_id"] from the fixture + + def test_measurements_without_lot_or_serial_still_scope_to_the_operator( + self, logged_in_client, mock_api_client, + ): + """No lot/serial in session (recipe does not require them): the + operator filter alone still narrows the summary to this run.""" + mock_api_client.get.side_effect = [ + {"id": 1, "code": "REC-001", "name": "Test Recipe", "version": 1}, + [{"id": 1, "title": "Task 1", "order_index": 0, "subtasks": []}], + {"items": [], "pages": 1}, + ] + + resp = logged_in_client.get("/measure/complete/1?version_id=5") + assert resp.status_code == 200 + + endpoint, kwargs = mock_api_client.get.call_args_list[2] + params = kwargs["params"] + assert "lot_number" not in params + assert "serial_number" not in params + assert params["measured_by"] == 1 + + def test_measurements_beyond_500_are_paged_not_truncated( + self, logged_in_client, mock_api_client, + ): + """#30: a cycle over pages instead of a single capped per_page=500 call.""" + subtask = {"id": 1, "nominal": 5.0, "unit": "mm"} + page1 = { + "items": [{"id": i, "subtask_id": 1, "value": 5.0} for i in range(500)], + "pages": 2, + } + page2 = {"items": [{"id": 500, "subtask_id": 1, "value": 5.0}], "pages": 2} + mock_api_client.get.side_effect = [ + {"id": 1, "code": "REC-001", "name": "Test Recipe", "version": 1}, + [{"id": 1, "title": "Task 1", "order_index": 0, "subtasks": [subtask]}], + page1, + page2, + ] + + resp = logged_in_client.get("/measure/complete/1?version_id=5") + assert resp.status_code == 200 + assert mock_api_client.get.call_count == 4 + first_page_call = mock_api_client.get.call_args_list[2] + second_page_call = mock_api_client.get.call_args_list[3] + assert first_page_call.kwargs["params"]["page"] == 1 + assert second_page_call.kwargs["params"]["page"] == 2 + class TestOutOfTolerance: """Proxies for point 5: the capoturno's answer has to reach the measurement."""