"""Point 10: a task left half done has to look different from one never opened. The task list showed both the same way, so an operator coming back to a recipe had to remember where they had got to - or measure something twice. These tests are about the count the list now puts on each card. """ from httpx import AsyncClient from sqlalchemy import select from src.backend.models.orm.recipe import RecipeVersion from src.backend.models.orm.station import Station from src.backend.models.orm.task import RecipeSubtask, RecipeTask from src.backend.tests.conftest import auth_headers, create_test_recipe async def _version_and_task(db_session, recipe_id: int): version = (await db_session.execute( select(RecipeVersion).where( RecipeVersion.recipe_id == recipe_id, RecipeVersion.is_current == True, # noqa: E712 ) )).scalar_one() task = (await db_session.execute( select(RecipeTask).where(RecipeTask.version_id == version.id) )).scalars().first() return version, task async def _add_quote(db_session, task_id: int, marker: int): quote = RecipeSubtask( task_id=task_id, marker_number=marker, description=f"Quota {marker}", nominal=10.0, utl=10.5, uwl=10.3, lwl=9.7, ltl=9.5, unit="mm", ) db_session.add(quote) await db_session.commit() await db_session.refresh(quote) return quote async def _measure(client, user, subtask_id, version_id, value=10.0): return await client.post( "/api/measurements/", headers=auth_headers(user), json={"subtask_id": subtask_id, "version_id": version_id, "value": value}, ) async def _progress(client, user, version_id): resp = await client.get( "/api/measurements/task-progress", headers=auth_headers(user), params={"version_id": version_id}, ) assert resp.status_code == 200 return {row["task_id"]: row for row in resp.json()["tasks"]} async def test_untouched_task_reads_as_none( client: AsyncClient, db_session, measurement_tec_user, ): recipe = await create_test_recipe(db_session, measurement_tec_user.id) version, task = await _version_and_task(db_session, recipe.id) rows = await _progress(client, measurement_tec_user, version.id) assert rows[task.id]["state"] == "none" assert rows[task.id]["measured"] == 0 async def test_half_measured_task_reads_as_partial( client: AsyncClient, db_session, measurement_tec_user, ): recipe = await create_test_recipe(db_session, measurement_tec_user.id) version, task = await _version_and_task(db_session, recipe.id) first = (await db_session.execute( select(RecipeSubtask).where(RecipeSubtask.task_id == task.id) )).scalars().first() await _add_quote(db_session, task.id, 2) await _measure(client, measurement_tec_user, first.id, version.id) rows = await _progress(client, measurement_tec_user, version.id) assert rows[task.id] == { "task_id": task.id, "quotes": 2, "measured": 1, "state": "partial", } async def test_every_quote_taken_reads_as_complete( client: AsyncClient, db_session, measurement_tec_user, ): recipe = await create_test_recipe(db_session, measurement_tec_user.id) version, task = await _version_and_task(db_session, recipe.id) first = (await db_session.execute( select(RecipeSubtask).where(RecipeSubtask.task_id == task.id) )).scalars().first() second = await _add_quote(db_session, task.id, 2) await _measure(client, measurement_tec_user, first.id, version.id) await _measure(client, measurement_tec_user, second.id, version.id) rows = await _progress(client, measurement_tec_user, version.id) assert rows[task.id]["state"] == "complete" assert rows[task.id]["measured"] == 2 async def test_measuring_the_same_quote_twice_does_not_inflate_the_count( client: AsyncClient, db_session, measurement_tec_user, ): """Two readings of one quote are one quote done, not two. Re-measuring is allowed - it is how an operator recovers from a caliper that slipped - and counting attempts would show a task of two quotes as finished after one of them had been measured twice. """ recipe = await create_test_recipe(db_session, measurement_tec_user.id) version, task = await _version_and_task(db_session, recipe.id) first = (await db_session.execute( select(RecipeSubtask).where(RecipeSubtask.task_id == task.id) )).scalars().first() await _add_quote(db_session, task.id, 2) await _measure(client, measurement_tec_user, first.id, version.id, 10.0) await _measure(client, measurement_tec_user, first.id, version.id, 10.1) rows = await _progress(client, measurement_tec_user, version.id) assert rows[task.id]["measured"] == 1 assert rows[task.id]["state"] == "partial" async def test_documental_tasks_are_left_out( client: AsyncClient, db_session, measurement_tec_user, ): """A note cannot be half read; carrying it as unfinished for ever is noise.""" recipe = await create_test_recipe(db_session, measurement_tec_user.id) version, task = await _version_and_task(db_session, recipe.id) note = RecipeTask( version_id=version.id, order_index=5, title="Leggere prima", task_type="note", ) db_session.add(note) await db_session.commit() await db_session.refresh(note) rows = await _progress(client, measurement_tec_user, version.id) assert note.id not in rows assert task.id in rows async def test_a_measurement_task_without_quotes_is_not_reported_as_missing( client: AsyncClient, db_session, measurement_tec_user, ): """Declared as a measurement, no quotes entered yet: nothing to take.""" recipe = await create_test_recipe(db_session, measurement_tec_user.id) version, _task = await _version_and_task(db_session, recipe.id) empty = RecipeTask( version_id=version.id, order_index=6, title="Da compilare", task_type="measure", ) db_session.add(empty) await db_session.commit() await db_session.refresh(empty) rows = await _progress(client, measurement_tec_user, version.id) assert rows[empty.id]["state"] == "empty" assert rows[empty.id]["quotes"] == 0 async def test_another_operators_work_is_not_counted_as_yours( client: AsyncClient, db_session, measurement_tec_user, admin_user, ): """Outside a production the boundary is the operator, as it is everywhere else.""" recipe = await create_test_recipe(db_session, measurement_tec_user.id) version, task = await _version_and_task(db_session, recipe.id) first = (await db_session.execute( select(RecipeSubtask).where(RecipeSubtask.task_id == task.id) )).scalars().first() await _measure(client, admin_user, first.id, version.id) rows = await _progress(client, measurement_tec_user, version.id) assert rows[task.id]["state"] == "none" async def test_inside_a_production_the_run_is_the_boundary( client: AsyncClient, db_session, measurement_tec_user, admin_user, ): """A quote measured before the production opened is not part of it. The list is read during the run, and carrying yesterday's readings into it would show a task as finished that this production has not touched. """ recipe = await create_test_recipe(db_session, measurement_tec_user.id) version, task = await _version_and_task(db_session, recipe.id) first = (await db_session.execute( select(RecipeSubtask).where(RecipeSubtask.task_id == task.id) )).scalars().first() # Measured outside any production. await _measure(client, measurement_tec_user, first.id, version.id) station = Station( code="ST-PROG", name="Stazione", active=True, created_by=admin_user.id, ) db_session.add(station) await db_session.commit() run = (await client.post( "/api/production-runs", headers=auth_headers(measurement_tec_user), json={"station_code": station.code, "recipe_id": recipe.id}, )).json() resp = await client.get( "/api/measurements/task-progress", headers=auth_headers(measurement_tec_user), params={"version_id": version.id, "production_run_id": run["id"]}, ) assert resp.status_code == 200 rows = {row["task_id"]: row for row in resp.json()["tasks"]} assert rows[task.id]["state"] == "none" await client.post( "/api/measurements/", headers=auth_headers(measurement_tec_user), json={ "subtask_id": first.id, "version_id": version.id, "value": 10.0, "production_run_id": run["id"], }, ) resp = await client.get( "/api/measurements/task-progress", headers=auth_headers(measurement_tec_user), params={"version_id": version.id, "production_run_id": run["id"]}, ) rows = {row["task_id"]: row for row in resp.json()["tasks"]} assert rows[task.id]["state"] == "complete" async def test_progress_needs_a_measurement_role( client: AsyncClient, db_session, maker_user, measurement_tec_user, ): recipe = await create_test_recipe(db_session, measurement_tec_user.id) version, _task = await _version_and_task(db_session, recipe.id) resp = await client.get( "/api/measurements/task-progress", headers=auth_headers(maker_user), params={"version_id": version.id}, ) assert resp.status_code == 403