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:
@@ -4,7 +4,7 @@ Deliberately stateless: nothing lives in process memory. With one station app
|
||||
installed per PC the database is the only shared place, and any station app must be
|
||||
able to ask "what is going on here?" and get the same answer.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.backend.api.middleware.api_key import get_current_user
|
||||
@@ -29,6 +29,21 @@ _DERIVED = {
|
||||
}
|
||||
|
||||
|
||||
async def _require_run_operator(user: User = Depends(get_current_user)) -> User:
|
||||
"""Open/advance/pause/resume/close a run: MeasurementTec, Supervisor, or admin.
|
||||
|
||||
A Maker with no floor role could otherwise start and drive a production the
|
||||
same way an operator does (gitea#24) - reading a run stays open to any
|
||||
authenticated user, only the lifecycle actions are gated.
|
||||
"""
|
||||
if user.is_admin or user.has_role("MeasurementTec") or user.has_role("Supervisor"):
|
||||
return user
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="MeasurementTec or Supervisor role required",
|
||||
)
|
||||
|
||||
|
||||
async def _as_response(db: AsyncSession, run: ProductionRun) -> ProductionRunResponse:
|
||||
"""The stored run plus everything the client cannot work out on its own."""
|
||||
return ProductionRunResponse(
|
||||
@@ -43,7 +58,7 @@ async def _as_response(db: AsyncSession, run: ProductionRun) -> ProductionRunRes
|
||||
@router.post("", response_model=ProductionRunResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def open_production_run(
|
||||
data: ProductionRunCreate,
|
||||
user: User = Depends(get_current_user),
|
||||
user: User = Depends(_require_run_operator),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Start a production at a station."""
|
||||
@@ -85,7 +100,7 @@ async def get_production_run(
|
||||
async def complete_measurement_cycle(
|
||||
run_id: int,
|
||||
payload: CycleCompletePayload | None = None,
|
||||
user: User = Depends(get_current_user),
|
||||
user: User = Depends(_require_run_operator),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Record a finished measurement task; the last one restarts the interval."""
|
||||
@@ -102,7 +117,7 @@ async def complete_measurement_cycle(
|
||||
async def remeasure_in_cycle(
|
||||
run_id: int,
|
||||
payload: RemeasurePayload | None = None,
|
||||
user: User = Depends(get_current_user),
|
||||
user: User = Depends(_require_run_operator),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Turn the piece over and measure again, without closing the cycle."""
|
||||
@@ -119,7 +134,7 @@ async def remeasure_in_cycle(
|
||||
async def pause_production_run(
|
||||
run_id: int,
|
||||
action: SupervisorAction,
|
||||
user: User = Depends(get_current_user),
|
||||
user: User = Depends(_require_run_operator),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Fermo linea - requires a supervisor."""
|
||||
@@ -135,7 +150,7 @@ async def pause_production_run(
|
||||
async def resume_production_run(
|
||||
run_id: int,
|
||||
action: SupervisorAction,
|
||||
user: User = Depends(get_current_user),
|
||||
user: User = Depends(_require_run_operator),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Restart a stopped line - requires a supervisor."""
|
||||
@@ -151,7 +166,7 @@ async def resume_production_run(
|
||||
async def close_production_run(
|
||||
run_id: int,
|
||||
action: SupervisorAction,
|
||||
user: User = Depends(get_current_user),
|
||||
user: User = Depends(_require_run_operator),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Fine produzione - requires a supervisor. Stops the timer for good."""
|
||||
|
||||
Reference in New Issue
Block a user