fix(measure): riepilogo fine task filtrato per lotto/seriale/operatore, pagina oltre 500 (#30)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -354,6 +354,8 @@ def task_complete(recipe_id: int):
|
|||||||
"""Task completion summary with all measurements."""
|
"""Task completion summary with all measurements."""
|
||||||
# Retrieve version_id from query params
|
# Retrieve version_id from query params
|
||||||
version_id = request.args.get("version_id")
|
version_id = request.args.get("version_id")
|
||||||
|
lot_number = session.get("lot_number", "")
|
||||||
|
serial_number = session.get("serial_number", "")
|
||||||
|
|
||||||
# Load recipe for context
|
# Load recipe for context
|
||||||
recipe_resp = api_client.get(f"/api/recipes/{recipe_id}")
|
recipe_resp = api_client.get(f"/api/recipes/{recipe_id}")
|
||||||
@@ -382,15 +384,40 @@ def task_complete(recipe_id: int):
|
|||||||
# Load measurements if version_id provided
|
# Load measurements if version_id provided
|
||||||
measurements = []
|
measurements = []
|
||||||
if version_id:
|
if version_id:
|
||||||
|
# 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(
|
meas_resp = api_client.get(
|
||||||
"/api/measurements",
|
"/api/measurements", params={**params, "page": page},
|
||||||
params={"version_id": version_id, "per_page": 500},
|
|
||||||
)
|
)
|
||||||
if not (isinstance(meas_resp, dict) and meas_resp.get("error")):
|
if isinstance(meas_resp, dict) and meas_resp.get("error"):
|
||||||
raw = (
|
break
|
||||||
|
page_items = (
|
||||||
meas_resp if isinstance(meas_resp, list)
|
meas_resp if isinstance(meas_resp, list)
|
||||||
else meas_resp.get("items", [])
|
else meas_resp.get("items", [])
|
||||||
)
|
)
|
||||||
|
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
|
||||||
|
|
||||||
# Enrich each measurement with nested subtask data
|
# Enrich each measurement with nested subtask data
|
||||||
for m in raw:
|
for m in raw:
|
||||||
st = subtask_map.get(m.get("subtask_id"), {})
|
st = subtask_map.get(m.get("subtask_id"), {})
|
||||||
@@ -404,9 +431,6 @@ def task_complete(recipe_id: int):
|
|||||||
m["deviation"] = 0.0
|
m["deviation"] = 0.0
|
||||||
measurements = raw
|
measurements = raw
|
||||||
|
|
||||||
lot_number = session.get("lot_number", "")
|
|
||||||
serial_number = session.get("serial_number", "")
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"measure/task_complete.html",
|
"measure/task_complete.html",
|
||||||
recipe=recipe_resp,
|
recipe=recipe_resp,
|
||||||
|
|||||||
@@ -264,6 +264,77 @@ class TestTaskComplete:
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert b"productionClock(" in resp.data
|
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:
|
class TestOutOfTolerance:
|
||||||
"""Proxies for point 5: the capoturno's answer has to reach the measurement."""
|
"""Proxies for point 5: the capoturno's answer has to reach the measurement."""
|
||||||
|
|||||||
Reference in New Issue
Block a user