fix(production): ruolo e assegnazione stazione su apertura/avanzamento run (#24 backend)
api/routers/production.py: open/cycle/remeasure/pause/resume/close accettavano
qualunque utente autenticato (get_current_user). Nuova dipendenza locale
_require_run_operator (MeasurementTec o Supervisor o admin) sui sei endpoint
di ciclo vita; le letture (current, {run_id} GET) restano aperte a tutti gli
autenticati. Per pause/resume/close si aggiunge al controllo esistente delle
credenziali del supervisore in corpo, che riguarda un'altra persona (chi
autorizza), non chi opera.
services/production_service.py open_run: nessun controllo su recipe.active ne'
sull'assegnazione ricetta-stazione. Nuovo _require_recipe_assigned_to_station:
se la stazione ha almeno un'assegnazione (StationRecipeAssignment), la ricetta
deve esserci, altrimenti 409; una stazione senza alcuna assegnazione resta
libera - stessa regola del filtro in lettura di station_service.list_station_recipes
(join che senza righe non filtra nulla). Ricetta non active -> 409.
NON TOCCATO: src/frontend/flask_app/blueprints/measure.py (stesso filtro sul
lato Flask, lotto B2) e test_measure_station_filter.py - altro agente ci
lavora in parallelo sulla stessa cartella.
Test: src/backend/tests/test_production_runs.py, 6 nuovi test (ruolo su
open/pause, assegnazione rifiutata/accettata/libera, ricetta inattiva).
PROVA: uv run --extra server --extra dev pytest src/backend/tests/test_production_runs.py -q -> 34 passed
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from src.backend.models.api.production import ProductionRunCreate
|
||||
from src.backend.models.orm.production import ProductionEvent, ProductionRun
|
||||
from src.backend.models.orm.recipe import Recipe, RecipeVersion
|
||||
from src.backend.models.orm.station import Station
|
||||
from src.backend.models.orm.station import Station, StationRecipeAssignment
|
||||
from src.backend.models.orm.task import MEASURING_TASK_TYPES, RecipeTask
|
||||
from src.backend.models.orm.user import User
|
||||
from src.backend.services import (
|
||||
@@ -106,6 +106,29 @@ async def get_station_by_code(db: AsyncSession, code: str) -> Station:
|
||||
return station
|
||||
|
||||
|
||||
async def _require_recipe_assigned_to_station(
|
||||
db: AsyncSession, station_id: int, recipe_id: int,
|
||||
) -> None:
|
||||
"""A configured station only runs what it is assigned; an unconfigured one is free.
|
||||
|
||||
Mirrors station_service.list_station_recipes, the read side an operator's
|
||||
recipe list is already filtered by: a station with at least one assignment
|
||||
restricts to those recipes, one with none has not been set up to filter at
|
||||
all and stays open to any active recipe.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(StationRecipeAssignment.recipe_id).where(
|
||||
StationRecipeAssignment.station_id == station_id,
|
||||
)
|
||||
)
|
||||
assigned_ids = set(result.scalars().all())
|
||||
if assigned_ids and recipe_id not in assigned_ids:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="This recipe is not assigned to this station",
|
||||
)
|
||||
|
||||
|
||||
async def measurement_task_ids(db: AsyncSession, version_id: int) -> list[int]:
|
||||
"""Ids of the measurement tasks of a version, in the order they are executed.
|
||||
|
||||
@@ -199,6 +222,12 @@ async def open_run(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Recipe not found",
|
||||
)
|
||||
if not recipe.active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Cannot start a production run with an inactive recipe",
|
||||
)
|
||||
await _require_recipe_assigned_to_station(db, station.id, recipe.id)
|
||||
|
||||
if data.version_id is not None:
|
||||
version_row = await db.execute(
|
||||
|
||||
Reference in New Issue
Block a user