feat(measure): il fuori tolleranza si autorizza, non si chiude
Il punto 5 c'era a meta': la schermata apriva il modale del capoturno e bloccava l'avanzamento automatico, ma il modale si chiudeva con Annulla o con un click sullo sfondo, la misura era gia' salvata, e `pendingAdvance` veniva impostato e non letto da nessuno. Era una conferma, non uno sbarramento. Ora l'autorizzazione finisce sulla misura: due colonne, chi ha autorizzato e quando. Finche' una quota e' fuori tolleranza e nessuno l'ha autorizzata, il server rifiuta la misura della quota successiva e la chiusura del ciclo. Non e' la schermata a impedirlo: la schermata risparmia solo il viaggio. Rimisurare la stessa quota resta possibile — il calibro scivola, il pezzo si riposiziona — e una seconda lettura in tolleranza libera il blocco, perche' conta l'ultima lettura di ogni quota. Quante volte si possa riprovare e' il punto 4, e tutte le letture restano comunque a registro. Un warning non blocca: fuori dai limiti di attenzione ma dentro la tolleranza e' dentro la tolleranza. Ricaricare la pagina era il modo piu' semplice per scavalcare il vecchio gate. Non lo e' piu': la schermata chiede al server, all'apertura, se una quota sta aspettando, e si ritrova davanti lo stesso blocco. Sparisce /validate-supervisor, che verificava le credenziali e buttava via la risposta. Al suo posto un endpoint che le credenziali le usa per scrivere l'approvazione dove serve. Il controllo del capoturno si sposta in auth_service, accanto al resto delle credenziali: fermo linea, chiusura e fuori tolleranza fanno la stessa domanda, e solo una delle tre riguarda la produzione. Il file di statistica guadagna authorised_by e authorised_at: mostrava il fallimento e non la decisione, che e' la meta' che un auditor chiede. Migrazione 010: due colonne nullable sulle misure. Le righe esistenti restano nulle — retrodatare un'autorizzazione mai avvenuta sarebbe inventarsi un record di audit, e quelle produzioni sono chiuse da un pezzo. Test: +18 (309). Coprono il rifiuto della quota successiva, la rimisura ammessa, il rilascio del blocco con una lettura buona, il ciclo che non si chiude, il capoturno registrato sulla misura, le credenziali sbagliate e chi capoturno non e', il file di statistica, e il fatto che una produzione chiusa non blocchi la successiva. Aggiunto uploads/statistics/ al gitignore: i test che chiudono una produzione scrivevano nel repository. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,11 +19,13 @@ from src.backend.models.orm.recipe import RecipeVersion
|
||||
from src.backend.models.orm.setting import SystemSetting
|
||||
from src.backend.models.orm.user import User
|
||||
from src.backend.models.api.measurement import (
|
||||
MeasurementAuthorisation,
|
||||
MeasurementBatchCreate,
|
||||
MeasurementCreate,
|
||||
MeasurementListResponse,
|
||||
MeasurementResponse,
|
||||
)
|
||||
from src.backend.services import auth_service, measurement_service
|
||||
from src.backend.services.measurement_service import save_measurement
|
||||
|
||||
router = APIRouter(prefix="/api/measurements", tags=["measurements"])
|
||||
@@ -57,6 +59,60 @@ async def create_measurement(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/pending-authorisation", response_model=MeasurementResponse | None)
|
||||
async def get_pending_authorisation(
|
||||
version_id: int = Query(..., gt=0),
|
||||
production_run_id: int | None = Query(None, gt=0),
|
||||
user: User = Depends(require_measurement_tec),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""The out-of-tolerance measurement holding this operator up, or null.
|
||||
|
||||
Asked on load, so that reloading the page is not a way past the gate: the
|
||||
screen finds the same block waiting for it that the server enforces.
|
||||
"""
|
||||
blocking = await measurement_service.pending_authorisation(
|
||||
db, version_id, user.id, production_run_id,
|
||||
)
|
||||
return MeasurementResponse.model_validate(blocking) if blocking else None
|
||||
|
||||
|
||||
@router.post("/{measurement_id}/authorise", response_model=MeasurementResponse)
|
||||
async def authorise_measurement(
|
||||
measurement_id: int,
|
||||
action: MeasurementAuthorisation,
|
||||
user: User = Depends(require_measurement_tec),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Let an out-of-tolerance value stand, on the capoturno's name.
|
||||
|
||||
Credentials go to this endpoint rather than to a separate check because the
|
||||
approval has to end up attached to the measurement. A check whose answer is
|
||||
thrown away is what this point had before: a modal that closed.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Measurement).where(Measurement.id == measurement_id)
|
||||
)
|
||||
measurement = result.scalar_one_or_none()
|
||||
if measurement is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Measurement not found",
|
||||
)
|
||||
if measurement.pass_fail != "fail":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="This measurement is within tolerance: there is nothing to authorise",
|
||||
)
|
||||
|
||||
supervisor = await auth_service.authorise_supervisor(
|
||||
db, action.supervisor_username, action.supervisor_password,
|
||||
)
|
||||
measurement = await measurement_service.authorise_measurement(
|
||||
db, measurement, supervisor,
|
||||
)
|
||||
return MeasurementResponse.model_validate(measurement)
|
||||
|
||||
|
||||
@router.post("/batch", response_model=list[MeasurementResponse])
|
||||
async def create_measurement_batch(
|
||||
data: MeasurementBatchCreate,
|
||||
|
||||
@@ -20,7 +20,7 @@ from src.backend.models.api.production import (
|
||||
)
|
||||
from src.backend.models.orm.production import ProductionRun
|
||||
from src.backend.models.orm.user import User
|
||||
from src.backend.services import production_service
|
||||
from src.backend.services import auth_service, production_service
|
||||
|
||||
router = APIRouter(prefix="/api/production-runs", tags=["production"])
|
||||
|
||||
@@ -124,7 +124,7 @@ async def pause_production_run(
|
||||
):
|
||||
"""Fermo linea - requires a supervisor."""
|
||||
run = await production_service.get_run(db, run_id)
|
||||
supervisor = await production_service.authorise_supervisor(
|
||||
supervisor = await auth_service.authorise_supervisor(
|
||||
db, action.supervisor_username, action.supervisor_password,
|
||||
)
|
||||
run = await production_service.pause_run(db, run, user, supervisor, note=action.note)
|
||||
@@ -140,7 +140,7 @@ async def resume_production_run(
|
||||
):
|
||||
"""Restart a stopped line - requires a supervisor."""
|
||||
run = await production_service.get_run(db, run_id)
|
||||
supervisor = await production_service.authorise_supervisor(
|
||||
supervisor = await auth_service.authorise_supervisor(
|
||||
db, action.supervisor_username, action.supervisor_password,
|
||||
)
|
||||
run = await production_service.resume_run(db, run, user, supervisor, note=action.note)
|
||||
@@ -156,7 +156,7 @@ async def close_production_run(
|
||||
):
|
||||
"""Fine produzione - requires a supervisor. Stops the timer for good."""
|
||||
run = await production_service.get_run(db, run_id)
|
||||
supervisor = await production_service.authorise_supervisor(
|
||||
supervisor = await auth_service.authorise_supervisor(
|
||||
db, action.supervisor_username, action.supervisor_password,
|
||||
)
|
||||
run = await production_service.close_run(db, run, user, supervisor, note=action.note)
|
||||
|
||||
Reference in New Issue
Block a user